Skip to content
This repository was archived by the owner on Oct 29, 2024. It is now read-only.

Commit 8edbbb8

Browse files
authored
Merge pull request #391 from glimmerjs/bugfix-signature-master
Fix Signature handling for unions and generics
2 parents 66298e1 + 006323e commit 8edbbb8

File tree

2 files changed

+74
-33
lines changed

2 files changed

+74
-33
lines changed

packages/@glimmer/component/addon/-private/component.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,7 @@ type ArgsFor<S> = 'Args' extends keyof S
6060
: { Named: S['Args']; Positional: [] }
6161
: { Named: EmptyObject; Positional: [] };
6262

63-
/**
64-
* Given any allowed shorthand form of a signature, desugars it to its full
65-
* expanded type.
66-
*
67-
* @internal This is only exported so we can avoid duplicating it in
68-
* [Glint](https://github.com/typed-ember/glint) or other such tooling. It is
69-
* *not* intended for public usage, and the specific mechanics it uses may
70-
* change at any time. Although the signature produced by is part of Glimmer's
71-
* public API the existence and mechanics of this specific symbol are *not*,
72-
* so ***DO NOT RELY ON IT***.
73-
*/
74-
export type ExpandSignature<T> = {
63+
type _ExpandSignature<T> = {
7564
Element: GetOrElse<T, 'Element', null>;
7665
Args: keyof T extends 'Args' | 'Element' | 'Blocks' // Is this a `Signature`?
7766
? ArgsFor<T> // Then use `Signature` args
@@ -84,6 +73,24 @@ export type ExpandSignature<T> = {
8473
}
8574
: EmptyObject;
8675
};
76+
/**
77+
* Given any allowed shorthand form of a signature, desugars it to its full
78+
* expanded type.
79+
*
80+
* @internal This is only exported so we can avoid duplicating it in
81+
* [Glint](https://github.com/typed-ember/glint) or other such tooling. It is
82+
* *not* intended for public usage, and the specific mechanics it uses may
83+
* change at any time. Although the signature produced by is part of Glimmer's
84+
* public API the existence and mechanics of this specific symbol are *not*,
85+
* so ***DO NOT RELY ON IT***.
86+
*/
87+
// The conditional type here is because TS applies conditional types
88+
// distributively. This means that for union types, checks like `keyof T` get
89+
// all the keys from all elements of the union, instead of ending up as `never`
90+
// and then always falling into the `Signature` path instead of falling back to
91+
// the legacy args handling path.
92+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
93+
export type ExpandSignature<T> = T extends any ? _ExpandSignature<T> : never;
8794

8895
/**
8996
* @internal we use this type for convenience internally; inference means users

test/types/component-test.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,71 @@
11
import { expectTypeOf } from 'expect-type';
2+
3+
// Intentionally checking the shape of the exports *and* the export itself.
24
import * as gc from '@glimmer/component';
5+
// tslint:disable-next-line: no-duplicate-imports
6+
import Component from '@glimmer/component';
37

48
// Imported from non-public-API so we can check that we are publishing what we
59
// expect to be -- and this keeps us honest about the fact that if we *change*
610
// this import location, we've broken any existing declarations published using
711
// the current type signatures.
812
import { EmptyObject } from '@glimmer/component/addon/-private/component';
913

10-
const Component = gc.default;
14+
declare let basicComponent: Component;
15+
expectTypeOf(basicComponent).toHaveProperty('args');
16+
expectTypeOf(basicComponent).toHaveProperty('isDestroying');
17+
expectTypeOf(basicComponent).toHaveProperty('isDestroyed');
18+
expectTypeOf(basicComponent).toHaveProperty('willDestroy');
19+
expectTypeOf(basicComponent.isDestroying).toEqualTypeOf<boolean>();
20+
expectTypeOf(basicComponent.isDestroyed).toEqualTypeOf<boolean>();
21+
expectTypeOf(basicComponent.willDestroy).toEqualTypeOf<() => void>();
1122

1223
expectTypeOf(gc).toHaveProperty('default');
1324
expectTypeOf(gc.default).toEqualTypeOf<typeof Component>();
1425

15-
type Args = {
26+
type LegacyArgs = {
1627
foo: number;
1728
};
1829

19-
const componentWithLegacyArgs = new Component<Args>({}, { foo: 123 });
20-
expectTypeOf(componentWithLegacyArgs).toHaveProperty('args');
21-
expectTypeOf(componentWithLegacyArgs).toHaveProperty('isDestroying');
22-
expectTypeOf(componentWithLegacyArgs).toHaveProperty('isDestroyed');
23-
expectTypeOf(componentWithLegacyArgs).toHaveProperty('willDestroy');
24-
expectTypeOf(componentWithLegacyArgs.args).toEqualTypeOf<Readonly<Args>>();
25-
expectTypeOf(componentWithLegacyArgs.isDestroying).toEqualTypeOf<boolean>();
26-
expectTypeOf(componentWithLegacyArgs.isDestroyed).toEqualTypeOf<boolean>();
27-
expectTypeOf(componentWithLegacyArgs.willDestroy).toEqualTypeOf<() => void>();
30+
const componentWithLegacyArgs = new Component<LegacyArgs>({}, { foo: 123 });
31+
expectTypeOf(componentWithLegacyArgs.args).toEqualTypeOf<Readonly<LegacyArgs>>();
32+
33+
// Here, we are testing that the types propertly distribute over union types,
34+
// generics which extend other types, etc.
35+
// Here, we are testing that the types propertly distribute over union types,
36+
// generics which extend other types, etc.
37+
type LegacyArgsDistributive = { foo: number } | { bar: string; baz: boolean };
38+
39+
const legacyArgsDistributiveA = new Component<LegacyArgsDistributive>({}, { foo: 123 });
40+
expectTypeOf(legacyArgsDistributiveA.args).toEqualTypeOf<Readonly<LegacyArgsDistributive>>();
41+
const legacyArgsDistributiveB = new Component<LegacyArgsDistributive>(
42+
{},
43+
{ bar: 'hello', baz: true }
44+
);
45+
expectTypeOf(legacyArgsDistributiveB.args).toEqualTypeOf<Readonly<LegacyArgsDistributive>>();
46+
47+
interface ExtensibleLegacy<T> {
48+
value: T;
49+
extras: boolean;
50+
funThings: string[];
51+
}
52+
53+
class WithExtensibleLegacy<T extends ExtensibleLegacy<unknown>> extends Component<T> {}
54+
declare const withExtensibleLegacy: WithExtensibleLegacy<ExtensibleLegacy<unknown>>;
55+
expectTypeOf(withExtensibleLegacy.args.value).toEqualTypeOf<unknown>();
56+
expectTypeOf(withExtensibleLegacy.args.extras).toEqualTypeOf<boolean>();
57+
expectTypeOf(withExtensibleLegacy.args.funThings).toEqualTypeOf<string[]>();
58+
59+
class WithExtensibleLegacySubclass extends WithExtensibleLegacy<ExtensibleLegacy<string>> {}
60+
declare const withExtensibleLegacySubclass: WithExtensibleLegacySubclass;
61+
expectTypeOf(withExtensibleLegacySubclass.args.value).toEqualTypeOf<string>();
2862

2963
interface ArgsOnly {
30-
Args: Args;
64+
Args: LegacyArgs;
3165
}
3266

3367
const componentWithArgsOnly = new Component<ArgsOnly>({}, { foo: 123 });
34-
expectTypeOf(componentWithArgsOnly.args).toEqualTypeOf<Readonly<Args>>();
68+
expectTypeOf(componentWithArgsOnly.args).toEqualTypeOf<Readonly<LegacyArgs>>();
3569

3670
interface ElementOnly {
3771
Element: HTMLParagraphElement;
@@ -55,33 +89,33 @@ const componentWithBlockOnly = new Component<BlockOnlySig>({}, {});
5589
expectTypeOf(componentWithBlockOnly.args).toEqualTypeOf<Readonly<EmptyObject>>();
5690

5791
interface ArgsAndBlocks {
58-
Args: Args;
92+
Args: LegacyArgs;
5993
Blocks: Blocks;
6094
}
6195

6296
const componentwithArgsAndBlocks = new Component<ArgsAndBlocks>({}, { foo: 123 });
63-
expectTypeOf(componentwithArgsAndBlocks.args).toEqualTypeOf<Readonly<Args>>();
97+
expectTypeOf(componentwithArgsAndBlocks.args).toEqualTypeOf<Readonly<LegacyArgs>>();
6498

6599
interface ArgsAndEl {
66-
Args: Args;
100+
Args: LegacyArgs;
67101
Element: HTMLParagraphElement;
68102
}
69103

70104
const componentwithArgsAndEl = new Component<ArgsAndEl>({}, { foo: 123 });
71-
expectTypeOf(componentwithArgsAndEl.args).toEqualTypeOf<Readonly<Args>>();
105+
expectTypeOf(componentwithArgsAndEl.args).toEqualTypeOf<Readonly<LegacyArgs>>();
72106

73107
interface FullShortSig {
74-
Args: Args;
108+
Args: LegacyArgs;
75109
Element: HTMLParagraphElement;
76110
Blocks: Blocks;
77111
}
78112

79113
const componentWithFullShortSig = new Component<FullShortSig>({}, { foo: 123 });
80-
expectTypeOf(componentWithFullShortSig.args).toEqualTypeOf<Readonly<Args>>();
114+
expectTypeOf(componentWithFullShortSig.args).toEqualTypeOf<Readonly<LegacyArgs>>();
81115

82116
interface FullLongSig {
83117
Args: {
84-
Named: Args;
118+
Named: LegacyArgs;
85119
Positional: [];
86120
};
87121
Element: HTMLParagraphElement;
@@ -95,4 +129,4 @@ interface FullLongSig {
95129
}
96130

97131
const componentWithFullSig = new Component<FullLongSig>({}, { foo: 123 });
98-
expectTypeOf(componentWithFullSig.args).toEqualTypeOf<Readonly<Args>>();
132+
expectTypeOf(componentWithFullSig.args).toEqualTypeOf<Readonly<LegacyArgs>>();

0 commit comments

Comments
 (0)