diff --git a/packages/@glimmer/component/addon/-private/component.ts b/packages/@glimmer/component/addon/-private/component.ts index 306782807..53cf6c808 100644 --- a/packages/@glimmer/component/addon/-private/component.ts +++ b/packages/@glimmer/component/addon/-private/component.ts @@ -60,18 +60,7 @@ type ArgsFor = 'Args' extends keyof S : { Named: S['Args']; Positional: [] } : { Named: EmptyObject; Positional: [] }; -/** - * Given any allowed shorthand form of a signature, desugars it to its full - * expanded type. - * - * @internal This is only exported so we can avoid duplicating it in - * [Glint](https://github.com/typed-ember/glint) or other such tooling. It is - * *not* intended for public usage, and the specific mechanics it uses may - * change at any time. Although the signature produced by is part of Glimmer's - * public API the existence and mechanics of this specific symbol are *not*, - * so ***DO NOT RELY ON IT***. - */ -export type ExpandSignature = { +type _ExpandSignature = { Element: GetOrElse; Args: keyof T extends 'Args' | 'Element' | 'Blocks' // Is this a `Signature`? ? ArgsFor // Then use `Signature` args @@ -84,6 +73,24 @@ export type ExpandSignature = { } : EmptyObject; }; +/** + * Given any allowed shorthand form of a signature, desugars it to its full + * expanded type. + * + * @internal This is only exported so we can avoid duplicating it in + * [Glint](https://github.com/typed-ember/glint) or other such tooling. It is + * *not* intended for public usage, and the specific mechanics it uses may + * change at any time. Although the signature produced by is part of Glimmer's + * public API the existence and mechanics of this specific symbol are *not*, + * so ***DO NOT RELY ON IT***. + */ +// The conditional type here is because TS applies conditional types +// distributively. This means that for union types, checks like `keyof T` get +// all the keys from all elements of the union, instead of ending up as `never` +// and then always falling into the `Signature` path instead of falling back to +// the legacy args handling path. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ExpandSignature = T extends any ? _ExpandSignature : never; /** * @internal we use this type for convenience internally; inference means users diff --git a/test/types/component-test.ts b/test/types/component-test.ts index f9f0f4030..5c38062ec 100644 --- a/test/types/component-test.ts +++ b/test/types/component-test.ts @@ -1,5 +1,9 @@ import { expectTypeOf } from 'expect-type'; + +// Intentionally checking the shape of the exports *and* the export itself. import * as gc from '@glimmer/component'; +// tslint:disable-next-line: no-duplicate-imports +import Component from '@glimmer/component'; // Imported from non-public-API so we can check that we are publishing what we // expect to be -- and this keeps us honest about the fact that if we *change* @@ -7,31 +11,61 @@ import * as gc from '@glimmer/component'; // the current type signatures. import { EmptyObject } from '@glimmer/component/addon/-private/component'; -const Component = gc.default; +declare let basicComponent: Component; +expectTypeOf(basicComponent).toHaveProperty('args'); +expectTypeOf(basicComponent).toHaveProperty('isDestroying'); +expectTypeOf(basicComponent).toHaveProperty('isDestroyed'); +expectTypeOf(basicComponent).toHaveProperty('willDestroy'); +expectTypeOf(basicComponent.isDestroying).toEqualTypeOf(); +expectTypeOf(basicComponent.isDestroyed).toEqualTypeOf(); +expectTypeOf(basicComponent.willDestroy).toEqualTypeOf<() => void>(); expectTypeOf(gc).toHaveProperty('default'); expectTypeOf(gc.default).toEqualTypeOf(); -type Args = { +type LegacyArgs = { foo: number; }; -const componentWithLegacyArgs = new Component({}, { foo: 123 }); -expectTypeOf(componentWithLegacyArgs).toHaveProperty('args'); -expectTypeOf(componentWithLegacyArgs).toHaveProperty('isDestroying'); -expectTypeOf(componentWithLegacyArgs).toHaveProperty('isDestroyed'); -expectTypeOf(componentWithLegacyArgs).toHaveProperty('willDestroy'); -expectTypeOf(componentWithLegacyArgs.args).toEqualTypeOf>(); -expectTypeOf(componentWithLegacyArgs.isDestroying).toEqualTypeOf(); -expectTypeOf(componentWithLegacyArgs.isDestroyed).toEqualTypeOf(); -expectTypeOf(componentWithLegacyArgs.willDestroy).toEqualTypeOf<() => void>(); +const componentWithLegacyArgs = new Component({}, { foo: 123 }); +expectTypeOf(componentWithLegacyArgs.args).toEqualTypeOf>(); + +// Here, we are testing that the types propertly distribute over union types, +// generics which extend other types, etc. +// Here, we are testing that the types propertly distribute over union types, +// generics which extend other types, etc. +type LegacyArgsDistributive = { foo: number } | { bar: string; baz: boolean }; + +const legacyArgsDistributiveA = new Component({}, { foo: 123 }); +expectTypeOf(legacyArgsDistributiveA.args).toEqualTypeOf>(); +const legacyArgsDistributiveB = new Component( + {}, + { bar: 'hello', baz: true } +); +expectTypeOf(legacyArgsDistributiveB.args).toEqualTypeOf>(); + +interface ExtensibleLegacy { + value: T; + extras: boolean; + funThings: string[]; +} + +class WithExtensibleLegacy> extends Component {} +declare const withExtensibleLegacy: WithExtensibleLegacy>; +expectTypeOf(withExtensibleLegacy.args.value).toEqualTypeOf(); +expectTypeOf(withExtensibleLegacy.args.extras).toEqualTypeOf(); +expectTypeOf(withExtensibleLegacy.args.funThings).toEqualTypeOf(); + +class WithExtensibleLegacySubclass extends WithExtensibleLegacy> {} +declare const withExtensibleLegacySubclass: WithExtensibleLegacySubclass; +expectTypeOf(withExtensibleLegacySubclass.args.value).toEqualTypeOf(); interface ArgsOnly { - Args: Args; + Args: LegacyArgs; } const componentWithArgsOnly = new Component({}, { foo: 123 }); -expectTypeOf(componentWithArgsOnly.args).toEqualTypeOf>(); +expectTypeOf(componentWithArgsOnly.args).toEqualTypeOf>(); interface ElementOnly { Element: HTMLParagraphElement; @@ -55,33 +89,33 @@ const componentWithBlockOnly = new Component({}, {}); expectTypeOf(componentWithBlockOnly.args).toEqualTypeOf>(); interface ArgsAndBlocks { - Args: Args; + Args: LegacyArgs; Blocks: Blocks; } const componentwithArgsAndBlocks = new Component({}, { foo: 123 }); -expectTypeOf(componentwithArgsAndBlocks.args).toEqualTypeOf>(); +expectTypeOf(componentwithArgsAndBlocks.args).toEqualTypeOf>(); interface ArgsAndEl { - Args: Args; + Args: LegacyArgs; Element: HTMLParagraphElement; } const componentwithArgsAndEl = new Component({}, { foo: 123 }); -expectTypeOf(componentwithArgsAndEl.args).toEqualTypeOf>(); +expectTypeOf(componentwithArgsAndEl.args).toEqualTypeOf>(); interface FullShortSig { - Args: Args; + Args: LegacyArgs; Element: HTMLParagraphElement; Blocks: Blocks; } const componentWithFullShortSig = new Component({}, { foo: 123 }); -expectTypeOf(componentWithFullShortSig.args).toEqualTypeOf>(); +expectTypeOf(componentWithFullShortSig.args).toEqualTypeOf>(); interface FullLongSig { Args: { - Named: Args; + Named: LegacyArgs; Positional: []; }; Element: HTMLParagraphElement; @@ -95,4 +129,4 @@ interface FullLongSig { } const componentWithFullSig = new Component({}, { foo: 123 }); -expectTypeOf(componentWithFullSig.args).toEqualTypeOf>(); +expectTypeOf(componentWithFullSig.args).toEqualTypeOf>();