From cccccd2f812c30be00c44e63ef18b326e1261fcc Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 1 Apr 2022 10:25:51 -0600 Subject: [PATCH 1/2] Add failing test for distributive signature types (cherry picked from commit 9c9f0232d36891f5632f5bfea6267a4c55549fff) --- test/types/component-test.ts | 76 ++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 21 deletions(-) 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>(); From 006323ec5851e16402c3d5ef364924c268c94bd2 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 1 Apr 2022 10:34:28 -0600 Subject: [PATCH 2/2] Fix type distributivity in Glimmer Component Signature When using a `keyof` type to check whether the type parameter for Glimmer Component is a `Signature` or the classic `Args`-only type, if we do not force TS to distribute over union types, it resolves the `keyof` check for union types with no shared members as `never`, and `never extends ` is always true. This in turn meant that for all such unions, as well as for cases where users were providing generic types which could then be further extended in their own subclasses. Accordingly, introduce the standard technique TypeScript provides for opting into distributivity: conditional types are documented to support exactly this. (cherry picked from commit ad429d570e997f6184e4948f69a24f927bdd122b) --- .../component/addon/-private/component.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) 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