diff --git a/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts b/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts index 83cffdfd1..279efcf16 100644 --- a/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts +++ b/packages/core/__tests__/language-server/diagnostic-augmentation.test.ts @@ -792,7 +792,7 @@ describe('Language Server: Diagnostic Augmentation', () => { }, { "message": "The {{component}} helper can't be used to directly invoke a component under Glint. Consider first binding the result to a variable, e.g. '{{#let (component 'component-name') as |ComponentName|}}' and then invoking it as ''. - Argument of type 'Invokable<(named?: PrebindArgs<{ message?: string | undefined; }, \\"message\\"> | undefined) => ComponentReturn, null>>' is not assignable to parameter of type 'ContentValue'.", + Argument of type 'Invokable<(named?: PrebindArgs<{ message?: string | undefined; }, \\"message\\"> | undefined) => ComponentReturn, unknown>>' is not assignable to parameter of type 'ContentValue'.", "range": { "end": { "character": 41, @@ -827,7 +827,7 @@ describe('Language Server: Diagnostic Augmentation', () => { }, { "message": "The {{component}} helper can't be used to directly invoke a component under Glint. Consider first binding the result to a variable, e.g. '{{#let (component 'component-name') as |ComponentName|}}' and then invoking it as '...'. - Argument of type 'Invokable<(named?: PrebindArgs<{ message?: string | undefined; }, \\"message\\"> | undefined) => ComponentReturn, null>>' is not assignable to parameter of type 'ComponentReturn'.", + Argument of type 'Invokable<(named?: PrebindArgs<{ message?: string | undefined; }, \\"message\\"> | undefined) => ComponentReturn, unknown>>' is not assignable to parameter of type 'ComponentReturn'.", "range": { "end": { "character": 56, diff --git a/packages/core/__tests__/language-server/diagnostics.test.ts b/packages/core/__tests__/language-server/diagnostics.test.ts index c50649001..d696e3eed 100644 --- a/packages/core/__tests__/language-server/diagnostics.test.ts +++ b/packages/core/__tests__/language-server/diagnostics.test.ts @@ -59,7 +59,7 @@ describe('Language Server: Diagnostics', () => { expect(templateDiagnostics).toMatchInlineSnapshot(` [ { - "message": "Property 'missingArg' does not exist on type 'EmptyObject'.", + "message": "Property 'missingArg' does not exist on type '{}'.", "range": { "end": { "character": 13, @@ -118,7 +118,7 @@ describe('Language Server: Diagnostics', () => { expect(diagnostics).toMatchObject([ { - message: "Property 'foo' does not exist on type 'EmptyObject'.", + message: "Property 'foo' does not exist on type '{}'.", source: 'glint:ts(2339)', }, ]); @@ -159,7 +159,7 @@ describe('Language Server: Diagnostics', () => { expect(diagnostics).toMatchObject([ { - message: "Property 'foo' does not exist on type 'EmptyObject'.", + message: "Property 'foo' does not exist on type '{}'.", source: 'glint:ts(2339)', }, ]); @@ -358,7 +358,7 @@ describe('Language Server: Diagnostics', () => { expect(server.getDiagnostics(project.fileURI('component-a.ts'))).toMatchInlineSnapshot(` [ { - "message": "Property 'version' does not exist on type 'EmptyObject'.", + "message": "Property 'version' does not exist on type '{}'.", "range": { "end": { "character": 36, diff --git a/packages/environment-ember-loose/-private/dsl/integration-declarations.d.ts b/packages/environment-ember-loose/-private/dsl/integration-declarations.d.ts index 5a963849b..c199eeedd 100644 --- a/packages/environment-ember-loose/-private/dsl/integration-declarations.d.ts +++ b/packages/environment-ember-loose/-private/dsl/integration-declarations.d.ts @@ -3,7 +3,6 @@ import { ComponentLike, HelperLike, ModifierLike } from '@glint/template'; import { Context, - EmptyObject, FlattenBlockParams, HasContext, TemplateContext, @@ -85,7 +84,7 @@ declare module '@ember/routing/route' { [Context]: TemplateContext< Controller & ModelField>, ModelField>, - EmptyObject, + {}, null >; } @@ -93,7 +92,7 @@ declare module '@ember/routing/route' { declare module '@ember/controller' { export default interface Controller { - [Context]: TemplateContext, EmptyObject, null>; + [Context]: TemplateContext, {}, null>; } } @@ -103,9 +102,7 @@ declare module '@ember/controller' { import '@ember/test-helpers'; import 'ember-cli-htmlbars'; -type TestTemplate = abstract new () => HasContext< - TemplateContext ->; +type TestTemplate = abstract new () => HasContext>; declare module '@ember/test-helpers' { export function render(template: TestTemplate): Promise; diff --git a/packages/environment-ember-loose/-private/intrinsics/component.d.ts b/packages/environment-ember-loose/-private/intrinsics/component.d.ts index fecd468ec..05ab250d8 100644 --- a/packages/environment-ember-loose/-private/intrinsics/component.d.ts +++ b/packages/environment-ember-loose/-private/intrinsics/component.d.ts @@ -1,7 +1,6 @@ import { WithBoundArgs } from '@glint/template'; import { ComponentReturn, - EmptyObject, DirectInvokable, InvokableInstance, Invokable, @@ -14,8 +13,8 @@ import { type ComponentNamedArgs = Component extends Invokable<(...args: infer Args) => any> ? Args extends [...positional: infer _, named?: infer Named] ? UnwrapNamedArgs - : EmptyObject - : EmptyObject; + : {} + : {}; type PartiallyAppliedComponent = Component extends Invokable ? WithBoundArgs< diff --git a/packages/environment-ember-loose/__tests__/type-tests/ember-component.test.ts b/packages/environment-ember-loose/__tests__/type-tests/ember-component.test.ts index 822de7a07..11a97b31e 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/ember-component.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/ember-component.test.ts @@ -7,7 +7,6 @@ import { NamedArgsMarker, } from '@glint/environment-ember-loose/-private/dsl'; import { expectTypeOf } from 'expect-type'; -import { EmptyObject } from '@glint/template/-private/integration'; import type { ComponentLike } from '@glint/template'; { @@ -19,12 +18,6 @@ import type { ComponentLike } from '@glint/template'; } } - resolve(NoArgsComponent)({ - // @ts-expect-error: extra named arg - foo: 'bar', - ...NamedArgsMarker, - }); - resolve(NoArgsComponent)( // @ts-expect-error: extra positional arg 'oops' @@ -50,7 +43,7 @@ import type { ComponentLike } from '@glint/template'; templateForBackingValue(this, function* (𝚪) { expectTypeOf(𝚪.this.foo).toEqualTypeOf(); expectTypeOf(𝚪.this).toEqualTypeOf(); - expectTypeOf(𝚪.args).toEqualTypeOf(); + expectTypeOf(𝚪.args).toEqualTypeOf<{}>(); }); } } diff --git a/packages/environment-ember-loose/__tests__/type-tests/glimmer-component.test.ts b/packages/environment-ember-loose/__tests__/type-tests/glimmer-component.test.ts index 21645ee9b..eb008e366 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/glimmer-component.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/glimmer-component.test.ts @@ -6,19 +6,12 @@ import { emitComponent, NamedArgsMarker, } from '@glint/environment-ember-loose/-private/dsl'; -import { EmptyObject } from '@glint/template/-private/integration'; import { expectTypeOf } from 'expect-type'; import { ComponentLike } from '@glint/template'; { class NoArgsComponent extends Component {} - resolve(NoArgsComponent)({ - // @ts-expect-error: extra named arg - foo: 'bar', - ...NamedArgsMarker, - }); - resolve(NoArgsComponent)( // @ts-expect-error: extra positional arg 'oops' @@ -44,7 +37,7 @@ import { ComponentLike } from '@glint/template'; templateForBackingValue(this, function* (𝚪) { expectTypeOf(𝚪.this.foo).toEqualTypeOf(); expectTypeOf(𝚪.this).toEqualTypeOf(); - expectTypeOf(𝚪.args).toEqualTypeOf(); + expectTypeOf(𝚪.args).toEqualTypeOf<{}>(); }); } } diff --git a/packages/environment-ember-loose/__tests__/type-tests/helper.test.ts b/packages/environment-ember-loose/__tests__/type-tests/helper.test.ts index c5335c2c4..84f3d1016 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/helper.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/helper.test.ts @@ -1,4 +1,4 @@ -import Helper, { helper, EmptyObject } from '@ember/component/helper'; +import Helper, { helper } from '@ember/component/helper'; import { resolve } from '@glint/environment-ember-loose/-private/dsl'; import { NamedArgsMarker, @@ -6,7 +6,7 @@ import { } from '@glint/environment-ember-loose/-private/dsl/without-function-resolution'; import { expectTypeOf } from 'expect-type'; import { HelperLike } from '@glint/template'; -import { EmptyObject as GlintEmptyObject, NamedArgs } from '@glint/template/-private/integration'; +import { NamedArgs } from '@glint/template/-private/integration'; // Functional helper: fixed signature params { @@ -56,7 +56,9 @@ import { EmptyObject as GlintEmptyObject, NamedArgs } from '@glint/template/-pri let definition = helper(([a, b]: [T, U]) => a || b); let or = resolve(definition); - expectTypeOf(or).toEqualTypeOf<{ (t: T, u: U, named?: NamedArgs): T | U }>(); + // Using `toMatch` rather than `toEqual` because helper resolution (currently) + // uses a special `EmptyObject` type to represent empty named args. + expectTypeOf(or).toMatchTypeOf<{ (t: T, u: U, named?: NamedArgs<{}>): T | U }>(); or('a', 'b', { // @ts-expect-error: extra named arg @@ -158,12 +160,12 @@ import { EmptyObject as GlintEmptyObject, NamedArgs } from '@glint/template/-pri let repeat = resolve(RepeatHelper); expectTypeOf(repeat).toEqualTypeOf<{ - (value: T, count?: number | undefined, args?: NamedArgs): Array; + (value: T, count?: number | undefined): Array; }>(); repeat( 'hello', - // @ts-expect-error: extra named arg + // @ts-expect-error: unexpected named args { word: 'hi', ...NamedArgsMarker } ); @@ -190,9 +192,7 @@ import { EmptyObject as GlintEmptyObject, NamedArgs } from '@glint/template/-pri let maybeString = resolve(MaybeStringHelper); - expectTypeOf(maybeString).toEqualTypeOf< - (args?: NamedArgs) => string | undefined - >(); + expectTypeOf(maybeString).toEqualTypeOf<() => string | undefined>(); } // Helpers are `HelperLike` diff --git a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/mut.test.ts b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/mut.test.ts index 9e86012ba..ae83527f6 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/mut.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/mut.test.ts @@ -14,8 +14,8 @@ expectTypeOf(fn(mut('hello'))).toEqualTypeOf<(value: string) => void>(); // @ts-expect-error: missing value mut(); +// @ts-expect-error: unexpected named args mut('hello', { - // @ts-expect-error: invalid named arg hello: 'hi', ...NamedArgsMarker, }); diff --git a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/outlet.test.ts b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/outlet.test.ts index df57b85ce..ee327b24a 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/outlet.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/outlet.test.ts @@ -9,8 +9,8 @@ expectTypeOf(outlet('outlet-name')).toEqualTypeOf(); // Nameless main outlet outlet(); +// @ts-expect-error: unexpected named args outlet('outlet-name', { - // @ts-expect-error: invalid named arg hello: 'hi', ...NamedArgsMarker, }); diff --git a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unbound.test.ts b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unbound.test.ts index 9adae144a..0f9bc4089 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unbound.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unbound.test.ts @@ -10,8 +10,8 @@ expectTypeOf(unbound(123)).toEqualTypeOf(); // @ts-expect-error: missing value unbound(); +// @ts-expect-error: unexpected named args unbound('hello', { - // @ts-expect-error: invalid named arg hello: 'hi', ...NamedArgsMarker, }); diff --git a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unique-id.test.ts b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unique-id.test.ts index 70c798b59..2204cde16 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unique-id.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/intrinsics/unique-id.test.ts @@ -6,8 +6,8 @@ let uniqueId = resolve(Globals['unique-id']); // Basic plumbing expectTypeOf(uniqueId()).toEqualTypeOf(); +// @ts-expect-error: unexpected named args uniqueId({ - // @ts-expect-error: invalid named arg hello: 'hi', ...NamedArgsMarker, }); diff --git a/packages/environment-ember-loose/__tests__/type-tests/route-and-controller.test.ts b/packages/environment-ember-loose/__tests__/type-tests/route-and-controller.test.ts index ae1a331bc..35a969e5d 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/route-and-controller.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/route-and-controller.test.ts @@ -1,7 +1,6 @@ import Route from '@ember/routing/route'; import Controller from '@ember/controller'; import { expectTypeOf } from 'expect-type'; -import { EmptyObject } from '@glint/template/-private/integration'; import { templateForBackingValue } from '../../-private/dsl'; class TestRoute extends Route { @@ -14,7 +13,7 @@ templateForBackingValue(TestRoute, function (routeContext) { expectTypeOf(routeContext.args).toEqualTypeOf<{ model: { message: string } }>(); expectTypeOf(routeContext.element).toBeNull(); expectTypeOf(routeContext.this).toEqualTypeOf(); - expectTypeOf(routeContext.blocks).toEqualTypeOf(); + expectTypeOf(routeContext.blocks).toEqualTypeOf<{}>(); }); class TestController extends Controller { @@ -29,5 +28,5 @@ templateForBackingValue(TestController, function (controllerContext) { expectTypeOf(controllerContext.args).toEqualTypeOf<{ model: { name: string; age: number } }>(); expectTypeOf(controllerContext.element).toBeNull(); expectTypeOf(controllerContext.this).toEqualTypeOf(); - expectTypeOf(controllerContext.blocks).toEqualTypeOf(); + expectTypeOf(controllerContext.blocks).toEqualTypeOf<{}>(); }); diff --git a/packages/environment-ember-loose/__tests__/type-tests/template-only.test.ts b/packages/environment-ember-loose/__tests__/type-tests/template-only.test.ts index 72952c74e..d5bf2bc9d 100644 --- a/packages/environment-ember-loose/__tests__/type-tests/template-only.test.ts +++ b/packages/environment-ember-loose/__tests__/type-tests/template-only.test.ts @@ -5,7 +5,7 @@ import { emitComponent, NamedArgsMarker, } from '@glint/environment-ember-loose/-private/dsl'; -import { ComponentReturn, EmptyObject, NamedArgs } from '@glint/template/-private/integration'; +import { ComponentReturn, NamedArgs } from '@glint/template/-private/integration'; import { expectTypeOf } from 'expect-type'; import { ComponentKeyword } from '../../-private/intrinsics/component'; import { ComponentLike, WithBoundArgs } from '@glint/template'; @@ -13,14 +13,7 @@ import { ComponentLike, WithBoundArgs } from '@glint/template'; { const NoArgsComponent = templateOnlyComponent(); - resolve(NoArgsComponent)({ - // @ts-expect-error: extra named arg - foo: 'bar', - ...NamedArgsMarker, - }); - resolve(NoArgsComponent)( - { ...NamedArgsMarker }, // @ts-expect-error: extra positional arg 'oops' ); @@ -38,9 +31,9 @@ import { ComponentLike, WithBoundArgs } from '@glint/template'; templateForBackingValue(NoArgsComponent, function (𝚪) { expectTypeOf(𝚪.this).toBeNull(); - expectTypeOf(𝚪.args).toEqualTypeOf(); - expectTypeOf(𝚪.element).toBeNull(); - expectTypeOf(𝚪.blocks).toEqualTypeOf(); + expectTypeOf(𝚪.args).toEqualTypeOf<{}>(); + expectTypeOf(𝚪.element).toBeUnknown(); + expectTypeOf(𝚪.blocks).toEqualTypeOf<{}>(); }); } @@ -124,7 +117,7 @@ import { ComponentLike, WithBoundArgs } from '@glint/template'; const CurriedWithNothing = resolve(componentKeyword)('curried-component'); expectTypeOf(resolve(CurriedWithNothing)).toEqualTypeOf< - (args: NamedArgs<{ a: string; b: number }>) => ComponentReturn + (args: NamedArgs<{ a: string; b: number }>) => ComponentReturn<{}> >(); const CurriedWithA = resolve(componentKeyword)('curried-component', { @@ -132,7 +125,7 @@ import { ComponentLike, WithBoundArgs } from '@glint/template'; ...NamedArgsMarker, }); expectTypeOf(resolve(CurriedWithA)).toEqualTypeOf< - (args: NamedArgs<{ a?: string; b: number }>) => ComponentReturn + (args: NamedArgs<{ a?: string; b: number }>) => ComponentReturn<{}> >(); } diff --git a/packages/environment-ember-template-imports/-private/dsl/index.d.ts b/packages/environment-ember-template-imports/-private/dsl/index.d.ts index c216cddab..01caac79d 100644 --- a/packages/environment-ember-template-imports/-private/dsl/index.d.ts +++ b/packages/environment-ember-template-imports/-private/dsl/index.d.ts @@ -9,7 +9,6 @@ import { AnyContext, AnyFunction, DirectInvokable, - EmptyObject, HasContext, InvokableInstance, Invoke, @@ -38,8 +37,8 @@ export declare const resolveOrReturn: ResolveOrReturn; import { TemplateOnlyComponent } from '@ember/component/template-only'; export declare function templateExpression< - Signature extends AnyFunction = () => ComponentReturn, - Context extends AnyContext = TemplateContext + Signature extends AnyFunction = () => ComponentReturn<{}>, + Context extends AnyContext = TemplateContext >( f: (𝚪: Context, χ: never) => void ): TemplateOnlyComponent & diff --git a/packages/environment-glimmerx/-private/dsl/index.d.ts b/packages/environment-glimmerx/-private/dsl/index.d.ts index ba87b0120..93b1a1060 100644 --- a/packages/environment-glimmerx/-private/dsl/index.d.ts +++ b/packages/environment-glimmerx/-private/dsl/index.d.ts @@ -27,7 +27,6 @@ import { AnyContext, AnyFunction, DirectInvokable, - EmptyObject, HasContext, InvokableInstance, Invoke, @@ -54,8 +53,8 @@ export declare const resolveOrReturn: ResolveOrReturn; import { TemplateComponentInstance } from '@glimmerx/component'; export declare function templateExpression< - Signature extends AnyFunction = () => ComponentReturn, - Context extends AnyContext = TemplateContext + Signature extends AnyFunction = () => ComponentReturn<{}>, + Context extends AnyContext = TemplateContext >( f: (𝚪: Context, χ: never) => void ): abstract new () => TemplateComponentInstance & diff --git a/packages/environment-glimmerx/__tests__/component.test.ts b/packages/environment-glimmerx/__tests__/component.test.ts index b4a747d3f..7a6c5428b 100644 --- a/packages/environment-glimmerx/__tests__/component.test.ts +++ b/packages/environment-glimmerx/__tests__/component.test.ts @@ -8,7 +8,7 @@ import { NamedArgsMarker, } from '@glint/environment-glimmerx/-private/dsl'; import { expectTypeOf } from 'expect-type'; -import { ComponentReturn, EmptyObject } from '@glint/template/-private/integration'; +import { ComponentReturn } from '@glint/template/-private/integration'; { class NoArgsComponent extends Component { @@ -17,12 +17,6 @@ import { ComponentReturn, EmptyObject } from '@glint/template/-private/integrati }); } - resolve(NoArgsComponent)({ - // @ts-expect-error: extra named arg - foo: 'bar', - ...NamedArgsMarker, - }); - resolve(NoArgsComponent)( // @ts-expect-error: bad positional arg 'oops' @@ -47,7 +41,7 @@ import { ComponentReturn, EmptyObject } from '@glint/template/-private/integrati static template = templateForBackingValue(this, function (𝚪) { expectTypeOf(𝚪.this.foo).toEqualTypeOf(); expectTypeOf(𝚪.this).toEqualTypeOf(); - expectTypeOf(𝚪.args).toEqualTypeOf(); + expectTypeOf(𝚪.args).toEqualTypeOf<{}>(); }); } @@ -152,11 +146,11 @@ import { ComponentReturn, EmptyObject } from '@glint/template/-private/integrati const NoAnnotationTC = templateExpression(function (𝚪) { expectTypeOf(𝚪.this).toBeVoid(); expectTypeOf(𝚪.element).toBeVoid(); - expectTypeOf(𝚪.args).toEqualTypeOf(); - expectTypeOf(𝚪.blocks).toEqualTypeOf(); + expectTypeOf(𝚪.args).toEqualTypeOf<{}>(); + expectTypeOf(𝚪.blocks).toEqualTypeOf<{}>(); }); - expectTypeOf(resolve(NoAnnotationTC)).toEqualTypeOf<() => ComponentReturn>(); + expectTypeOf(resolve(NoAnnotationTC)).toEqualTypeOf<() => ComponentReturn<{}>>(); } { @@ -173,7 +167,7 @@ import { ComponentReturn, EmptyObject } from '@glint/template/-private/integrati let YieldingTC: TC = templateExpression(function (𝚪) { expectTypeOf(𝚪.this).toEqualTypeOf(null); expectTypeOf(𝚪.args).toEqualTypeOf<{ values: Array }>(); - expectTypeOf(𝚪.element).toBeNull(); + expectTypeOf(𝚪.element).toBeUnknown(); expectTypeOf(𝚪.blocks).toEqualTypeOf(); if (𝚪.args.values.length) { diff --git a/packages/environment-glimmerx/__tests__/helper.test.ts b/packages/environment-glimmerx/__tests__/helper.test.ts index c00016548..6038db441 100644 --- a/packages/environment-glimmerx/__tests__/helper.test.ts +++ b/packages/environment-glimmerx/__tests__/helper.test.ts @@ -1,6 +1,6 @@ import { emitContent, NamedArgsMarker, resolve } from '@glint/environment-glimmerx/-private/dsl'; import { helper, fn as fnDefinition } from '@glimmerx/helper'; -import { EmptyObject, NamedArgs } from '@glint/template/-private/integration'; +import { NamedArgs } from '@glint/template/-private/integration'; import { expectTypeOf } from 'expect-type'; import '@glint/environment-glimmerx'; @@ -32,13 +32,14 @@ import '@glint/environment-glimmerx'; let definition = helper(([a, b]: [T, U]) => a || b); let or = resolve(definition); - expectTypeOf(or).toEqualTypeOf<{ (t: T, u: U, named?: NamedArgs): T | U }>(); + expectTypeOf(or).toEqualTypeOf<{ (t: T, u: U): T | U }>(); - or('a', 'b', { - // @ts-expect-error: extra named arg - hello: true, - ...NamedArgsMarker, - }); + or( + 'a', + 'b', + // @ts-expect-error: unexpected named args + { hello: true, ...NamedArgsMarker } + ); // @ts-expect-error: missing positional arg or('a'); diff --git a/packages/template/-private/dsl/emit.d.ts b/packages/template/-private/dsl/emit.d.ts index 06fd7b54c..4b832bf23 100644 --- a/packages/template/-private/dsl/emit.d.ts +++ b/packages/template/-private/dsl/emit.d.ts @@ -4,7 +4,6 @@ import { AnyContext, AnyFunction, ModifierReturn, - EmptyObject, HasContext, InvokableInstance, TemplateContext, @@ -80,8 +79,8 @@ export declare function emitComponent>( * environment's DSL export. */ export declare function templateExpression< - Signature extends AnyFunction = () => ComponentReturn, - Context extends AnyContext = TemplateContext + Signature extends AnyFunction = () => ComponentReturn<{}>, + Context extends AnyContext = TemplateContext >(f: (𝚪: Context, χ: never) => void): new () => InvokableInstance & HasContext; /* diff --git a/packages/template/-private/integration.d.ts b/packages/template/-private/integration.d.ts index ec0465b0e..ec1a935bd 100644 --- a/packages/template/-private/integration.d.ts +++ b/packages/template/-private/integration.d.ts @@ -6,9 +6,6 @@ // `ComponentLike`/`HelperLike`/`ModifierLike`, but these declarations are // the primitives on which those types are built. -declare const Empty: unique symbol; -export type EmptyObject = { [Empty]?: true }; - /** Any function, which is the tighest bound we can put on an object's `[Invoke]` field. */ export type AnyFunction = (...params: any) => any; diff --git a/packages/template/-private/signature.d.ts b/packages/template/-private/signature.d.ts index 24c91193a..7c4839dbb 100644 --- a/packages/template/-private/signature.d.ts +++ b/packages/template/-private/signature.d.ts @@ -2,7 +2,7 @@ // in userspace into our internal representation of an invokable's // function type signature. -import { EmptyObject, NamedArgs, UnwrapNamedArgs } from './integration'; +import { NamedArgs, UnwrapNamedArgs } from './integration'; /** * Given an "args hash" (e.g. `{ Named: {...}; Positional: [...] }`), @@ -10,7 +10,7 @@ import { EmptyObject, NamedArgs, UnwrapNamedArgs } from './integration'; */ export type InvokableArgs = [ ...positional: Constrain, Array, []>, - ...named: MaybeNamed>>> + ...named: MaybeNamed>> ]; /** Given a signature `S`, get back the normalized `Args` type. */ @@ -22,7 +22,7 @@ export type ComponentSignatureArgs = S extends { Positional?: unknown[]; } ? { - Named: Get; + Named: Get; Positional: Get; } : { @@ -30,7 +30,7 @@ export type ComponentSignatureArgs = S extends { Positional: []; } : { - Named: keyof S extends 'Args' | 'Blocks' | 'Element' ? EmptyObject : S; + Named: keyof S extends 'Args' | 'Blocks' | 'Element' ? {} : S; Positional: []; }; @@ -41,21 +41,24 @@ export type ComponentSignatureBlocks = S extends { Blocks: infer Blocks } ? { Params: { Positional: Blocks[Block] } } : Blocks[Block]; } - : EmptyObject; + : {}; /** Given a component signature `S`, get back the `Element` type. */ -export type ComponentSignatureElement = S extends { Element: infer Element } ? Element : null; - -// These shenanigans are necessary to get TS to report when named args -// are passed to a signature that doesn't expect any, because `{}` is -// special-cased in the type system not to trigger EPC. -export type GuardEmpty = T extends any ? (keyof T extends never ? EmptyObject : T) : never; +export type ComponentSignatureElement = S extends { Element: infer Element } + ? NonNullable extends never + ? unknown + : Element + : unknown; export type PrebindArgs> = NamedArgs< Omit, Args> & Partial, Args>> >; -export type MaybeNamed = {} extends UnwrapNamedArgs ? [named?: T] : [named: T]; +export type MaybeNamed = {} extends UnwrapNamedArgs + ? keyof UnwrapNamedArgs extends never + ? [] + : [named?: T] + : [named: T]; export type Get = K extends keyof T ? T[K] : Otherwise; export type Constrain = T extends Constraint ? T : Otherwise; diff --git a/packages/template/__tests__/component-like.test.ts b/packages/template/__tests__/component-like.test.ts index 6e3332855..7b2dfb3a9 100644 --- a/packages/template/__tests__/component-like.test.ts +++ b/packages/template/__tests__/component-like.test.ts @@ -2,21 +2,16 @@ import { ComponentLike, WithBoundArgs } from '@glint/template'; import { resolve, emitComponent, NamedArgsMarker } from '@glint/template/-private/dsl'; import { expectTypeOf } from 'expect-type'; import { ComponentReturn, NamedArgs } from '../-private/integration'; +import TestComponent from './test-component'; { const NoArgsComponent = {} as ComponentLike<{}>; + // @ts-expect-error: extra arg resolve(NoArgsComponent)({ - // @ts-expect-error: extra named arg - foo: 'bar', ...NamedArgsMarker, }); - resolve(NoArgsComponent)( - // @ts-expect-error: extra positional arg - 'oops' - ); - { const component = emitComponent(resolve(NoArgsComponent)()); @@ -103,7 +98,7 @@ import { ComponentReturn, NamedArgs } from '../-private/integration'; const PositionalArgsComponent = {} as ComponentLike; // @ts-expect-error: missing required positional arg - resolve(PositionalArgsComponent)(); + resolve(PositionalArgsComponent)({ ...NamedArgsMarker }); resolve(PositionalArgsComponent)( 'hello', @@ -138,3 +133,80 @@ import { ComponentReturn, NamedArgs } from '../-private/integration'; ) => ComponentReturn<{ default: [] }, HTMLCanvasElement> >(); } + +// Assignability +{ + // A component with no signaure is a `ComponentLike` with no signature + expectTypeOf(TestComponent<{}>).toMatchTypeOf(); + + // A component whose args are all optional is a `ComponentLike` with no signature + expectTypeOf(TestComponent<{ Args: { optional?: true } }>).toMatchTypeOf(); + + // A component with a required arg can't be used as a blank `ComponentLike` + expectTypeOf(TestComponent<{ Args: { optional: false } }>).not.toMatchTypeOf(); + + // A component that yields a given block can be used without ever passing any blocks + expectTypeOf(TestComponent<{ Blocks: { default: [string] } }>).toMatchTypeOf(); + + // A component that yields specific args can be used as one that cares about fewer of them + expectTypeOf(TestComponent<{ Blocks: { default: [string, number] } }>).toMatchTypeOf< + ComponentLike<{ Blocks: { default: [string, ...unknown[]] } }> + >(); + + // A component that never yields can't be used as one that accepts a specific block + expectTypeOf(TestComponent).not.toMatchTypeOf>(); + + // `T | null` is useful to humans to signify that a component might splat its ...attributes, + // but from a type perspective it's just the same as `T` + expectTypeOf>().toEqualTypeOf< + ComponentLike<{ Element: HTMLDivElement }> + >(); + + // Our canonical internal representation of a no-splattributes component's `Element` is `unknown` + expectTypeOf().toEqualTypeOf>(); + expectTypeOf>().toEqualTypeOf< + ComponentLike<{ Element: unknown }> + >(); + + // A component with all-optional args and any arbitrary element/blocks should be usable + // as a blank `ComponentLike`. + expectTypeOf( + TestComponent<{ + Args: { foo?: string }; + Element: HTMLImageElement; + Blocks: { default: [] }; + }> + ).toMatchTypeOf(); + + // Components are contravariant with their named `Args` type + expectTypeOf>().toMatchTypeOf< + ComponentLike<{ Args: { name: 'Dan' } }> + >(); + expectTypeOf>().not.toMatchTypeOf< + ComponentLike<{ Args: { name: string } }> + >(); + + // Components are contravariant with their positional `Args` type + expectTypeOf>().toMatchTypeOf< + ComponentLike<{ Args: { Positional: [name: 'Dan'] } }> + >(); + expectTypeOf>().not.toMatchTypeOf< + ComponentLike<{ Args: { Positional: [name: string] } }> + >(); + + // Components are covariant with their `Element` type + expectTypeOf>().toMatchTypeOf< + ComponentLike<{ Element: HTMLElement }> + >(); + expectTypeOf>().not.toMatchTypeOf< + ComponentLike<{ Element: HTMLAudioElement }> + >(); + + // Components are covariant with their `Blocks`' `Params` types + expectTypeOf(TestComponent<{ Blocks: { default: ['abc', 123] } }>).toMatchTypeOf< + ComponentLike<{ Blocks: { default: [string, number] } }> + >(); + expectTypeOf(TestComponent<{ Blocks: { default: [string, number] } }>).not.toMatchTypeOf< + ComponentLike<{ Blocks: { default: ['abc', 123] } }> + >(); +} diff --git a/packages/template/__tests__/helper-like.test.ts b/packages/template/__tests__/helper-like.test.ts index 368da189f..6d6d202c7 100644 --- a/packages/template/__tests__/helper-like.test.ts +++ b/packages/template/__tests__/helper-like.test.ts @@ -1,7 +1,7 @@ import { NamedArgsMarker, resolve } from '@glint/environment-ember-loose/-private/dsl'; import { expectTypeOf } from 'expect-type'; import { HelperLike, WithBoundArgs } from '@glint/template'; -import { EmptyObject, NamedArgs } from '../-private/integration'; +import { NamedArgs } from '../-private/integration'; // Fixed signature params { @@ -54,20 +54,13 @@ import { EmptyObject, NamedArgs } from '../-private/integration'; let definition!: new () => InstanceType>>; let or = resolve(definition); - expectTypeOf(or).toEqualTypeOf<{ (t: T, u: U, args?: NamedArgs): T | U }>(); - - or('a', 'b', { - // @ts-expect-error: extra named arg - hello: true, - ...NamedArgsMarker, - }); + expectTypeOf(or).toEqualTypeOf<{ (t: T, u: U): T | U }>(); or( 'a', 'b', - 'c', // @ts-expect-error: extra positional arg - { ...NamedArgsMarker } + 'c' ); expectTypeOf(or('a', 'b')).toEqualTypeOf(); @@ -88,3 +81,30 @@ import { EmptyObject, NamedArgs } from '../-private/integration'; (args: NamedArgs<{ age: number; name?: string }>) => string >(); } + +// Assignability +{ + // Helpers are contravariant with their named `Args` type + expectTypeOf>().toMatchTypeOf< + HelperLike<{ Args: { Named: { name: 'Dan' } } }> + >(); + expectTypeOf>().not.toMatchTypeOf< + HelperLike<{ Args: { Named: { name: string } } }> + >(); + + // Helpers are contravariant with their positional `Args` type + expectTypeOf>().toMatchTypeOf< + HelperLike<{ Args: { Positional: [name: 'Dan'] } }> + >(); + expectTypeOf>().not.toMatchTypeOf< + HelperLike<{ Args: { Positional: [name: string] } }> + >(); + + // Helpers are contravariant with their `Element` type + expectTypeOf>().toMatchTypeOf< + HelperLike<{ Return: string }> + >(); + expectTypeOf>().not.toMatchTypeOf< + HelperLike<{ Return: 'Hello, World' }> + >(); +} diff --git a/packages/template/__tests__/modifier-like.test.ts b/packages/template/__tests__/modifier-like.test.ts index 317a1f750..20be3c9a1 100644 --- a/packages/template/__tests__/modifier-like.test.ts +++ b/packages/template/__tests__/modifier-like.test.ts @@ -96,3 +96,30 @@ import { ModifierLike, WithBoundArgs } from '@glint/template'; ) => ModifierReturn >(); } + +// Assignability +{ + // Modifiers are contravariant with their named `Args` type + expectTypeOf>().toMatchTypeOf< + ModifierLike<{ Args: { Named: { name: 'Dan' } } }> + >(); + expectTypeOf>().not.toMatchTypeOf< + ModifierLike<{ Args: { Named: { name: string } } }> + >(); + + // Modifiers are contravariant with their positional `Args` type + expectTypeOf>().toMatchTypeOf< + ModifierLike<{ Args: { Positional: [name: 'Dan'] } }> + >(); + expectTypeOf>().not.toMatchTypeOf< + ModifierLike<{ Args: { Positional: [name: string] } }> + >(); + + // Modifiers are contravariant with their `Element` type + expectTypeOf>().toMatchTypeOf< + ModifierLike<{ Element: HTMLAudioElement }> + >(); + expectTypeOf>().not.toMatchTypeOf< + ModifierLike<{ Element: HTMLElement }> + >(); +} diff --git a/packages/template/__tests__/signature-test.test.ts b/packages/template/__tests__/signature-test.test.ts index 91d4516ca..fa59161d1 100644 --- a/packages/template/__tests__/signature-test.test.ts +++ b/packages/template/__tests__/signature-test.test.ts @@ -1,5 +1,4 @@ import { expectTypeOf } from 'expect-type'; -import { EmptyObject } from '../-private/integration'; import { ComponentSignatureArgs, ComponentSignatureBlocks, @@ -14,8 +13,8 @@ expectTypeOf>().toEqualTypeOf<{ Named: LegacyArgs; Positional: []; }>(); -expectTypeOf>().toEqualTypeOf(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf<{}>(); +expectTypeOf>().toEqualTypeOf(); // Here, we are testing that the types propertly distribute over union types, // generics which extend other types, etc. @@ -25,8 +24,8 @@ expectTypeOf>().toEqualTypeOf< | { Named: { foo: number }; Positional: [] } | { Named: { bar: string; baz: boolean }; Positional: [] } >(); -expectTypeOf>().toEqualTypeOf(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf<{}>(); +expectTypeOf>().toEqualTypeOf(); interface ArgsOnly { Args: LegacyArgs; @@ -36,18 +35,18 @@ expectTypeOf>().toEqualTypeOf<{ Named: LegacyArgs; Positional: []; }>(); -expectTypeOf>().toEqualTypeOf(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf<{}>(); +expectTypeOf>().toEqualTypeOf(); interface ElementOnly { Element: HTMLParagraphElement; } expectTypeOf>().toEqualTypeOf<{ - Named: EmptyObject; + Named: {}; Positional: []; }>(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf<{}>(); expectTypeOf>().toEqualTypeOf(); interface Blocks { @@ -60,7 +59,7 @@ interface BlockOnlySig { } expectTypeOf>().toEqualTypeOf<{ - Named: EmptyObject; + Named: {}; Positional: []; }>(); expectTypeOf>().toEqualTypeOf<{ @@ -75,7 +74,7 @@ expectTypeOf>().toEqualTypeOf<{ }; }; }>(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); interface ArgsAndBlocks { Args: LegacyArgs; @@ -98,7 +97,7 @@ expectTypeOf>().toEqualTypeOf<{ }; }; }>(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf(); interface ArgsAndEl { Args: LegacyArgs; @@ -109,7 +108,7 @@ expectTypeOf>().toEqualTypeOf<{ Named: LegacyArgs; Positional: []; }>(); -expectTypeOf>().toEqualTypeOf(); +expectTypeOf>().toEqualTypeOf<{}>(); expectTypeOf>().toEqualTypeOf(); interface FullShortSig { diff --git a/packages/template/__tests__/test-component.ts b/packages/template/__tests__/test-component.ts index f139696f8..cac098332 100644 --- a/packages/template/__tests__/test-component.ts +++ b/packages/template/__tests__/test-component.ts @@ -3,7 +3,7 @@ // well as simple examples of a helper and modifier. import { ComponentLike, ModifierLike } from '../-private/index'; -import { Context, EmptyObject, TemplateContext } from '../-private/integration'; +import { Context, TemplateContext } from '../-private/integration'; import { LetKeyword } from '../-private/keywords'; export default TestComponent; @@ -19,7 +19,7 @@ export declare const globals: { >; }; -type Get = K extends keyof T ? Exclude : Otherwise; +type Get = K extends keyof T ? Exclude : Otherwise; interface TestComponent extends InstanceType> {} declare class TestComponent { diff --git a/test-packages/ts-ember-app/tests/integration/types/empty-signature-test.ts b/test-packages/ts-ember-app/tests/integration/types/empty-signature-test.ts index e1c416ad4..bf74eeff6 100644 --- a/test-packages/ts-ember-app/tests/integration/types/empty-signature-test.ts +++ b/test-packages/ts-ember-app/tests/integration/types/empty-signature-test.ts @@ -27,8 +27,8 @@ module('Integration | Types | empty object signature members', function (hooks) <:named> - `);