Skip to content

Commit e324f50

Browse files
Merge pull request #604 from NullVoxPopuli/better-thunk-types
Fix type inference for the class-based resource thunk types
2 parents 5b73196 + e3900f7 commit e324f50

20 files changed

+765
-222
lines changed

ember-resources/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"ember-async-data": "^0.6.0",
144144
"ember-template-lint": "3.16.0",
145145
"eslint": "^7.32.0",
146+
"expect-type": "^0.13.0",
146147
"npm-run-all": "4.1.5",
147148
"rollup": "2.79.0",
148149
"rollup-plugin-terser": "^7.0.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expectTypeOf } from 'expect-type';
2+
3+
import { Resource } from '../class-based';
4+
5+
import type { ArgsFrom } from '../class-based/resource';
6+
import type { EmptyObject, Named, Positional } from '[core-types]';
7+
8+
// -----------------------------------------------------------
9+
// -----------------------------------------------------------
10+
// -----------------------------------------------------------
11+
12+
/**
13+
* -----------------------------------------------------------
14+
* Named
15+
* -----------------------------------------------------------
16+
*/
17+
expectTypeOf<Named<unknown>>().toEqualTypeOf<EmptyObject>();
18+
expectTypeOf<Named<{ named: { foo: number } }>>().toEqualTypeOf<{ foo: number }>();
19+
expectTypeOf<Named<{ Named: { foo: number } }>>().toEqualTypeOf<{ foo: number }>();
20+
expectTypeOf<Named<{ positional: [] }>>().toEqualTypeOf<EmptyObject>();
21+
expectTypeOf<Named<{ Positional: [] }>>().toEqualTypeOf<EmptyObject>();
22+
expectTypeOf<Named<{ Named: { foo: number }; Positional: [] }>>().toEqualTypeOf<{ foo: number }>();
23+
// @ts-expect-error
24+
expectTypeOf<Named<{ named: { foo: number }; Positional: [] }>>().toEqualTypeOf<{ foo: number }>();
25+
26+
/**
27+
* -----------------------------------------------------------
28+
* Positional
29+
* -----------------------------------------------------------
30+
*/
31+
expectTypeOf<Positional<unknown>>().toEqualTypeOf<[]>();
32+
expectTypeOf<Positional<{ positional: [number] }>>().toEqualTypeOf<[number]>();
33+
expectTypeOf<Positional<{ Positional: [number] }>>().toEqualTypeOf<[number]>();
34+
expectTypeOf<Positional<{ named: { foo: number } }>>().toEqualTypeOf<[]>();
35+
expectTypeOf<Positional<{ Named: { foo: number } }>>().toEqualTypeOf<[]>();
36+
expectTypeOf<Positional<{ Named: { foo: number }; Positional: [number] }>>().toEqualTypeOf<
37+
[number]
38+
>();
39+
// @ts-expect-error
40+
expectTypeOf<Positional<{ Named: { foo: number }; positional: [number] }>>().toEqualTypeOf<
41+
[number]
42+
>();
43+
44+
/**
45+
* -----------------------------------------------------------
46+
* ArgsFrom
47+
* -----------------------------------------------------------
48+
*/
49+
class Foo {
50+
foo = 'foo';
51+
}
52+
class Bar extends Resource {
53+
bar = 'bar';
54+
}
55+
class Baz extends Resource<{ Named: { baz: string } }> {
56+
baz = 'baz';
57+
}
58+
class Bax extends Resource<{ Positional: [string] }> {
59+
bax = 'bax';
60+
}
61+
// {} does not extend Resource
62+
// @ts-expect-error
63+
expectTypeOf<ArgsFrom<{}>>().toEqualTypeOf<never>();
64+
// unknown does not extend Resource
65+
// @ts-expect-error
66+
expectTypeOf<ArgsFrom<unknown>>().toEqualTypeOf<never>();
67+
// number does not extend Resource
68+
// @ts-expect-error
69+
expectTypeOf<ArgsFrom<2>>().toEqualTypeOf<never>();
70+
// string does not extend Resource
71+
// @ts-expect-error
72+
expectTypeOf<ArgsFrom<'string'>>().toEqualTypeOf<never>();
73+
// Foo does not extend Resource
74+
// @ts-expect-error
75+
expectTypeOf<ArgsFrom<Foo>>().toEqualTypeOf<never>();
76+
77+
expectTypeOf<ArgsFrom<Bar>>().toEqualTypeOf<unknown>();
78+
expectTypeOf<ArgsFrom<Baz>>().toEqualTypeOf<{ Named: { baz: string } }>();
79+
expectTypeOf<ArgsFrom<Bax>>().toEqualTypeOf<{ Positional: [string] }>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* These type tests are sorted alphabetically by the name of the type utility
3+
*/
4+
import { expectTypeOf } from 'expect-type';
5+
6+
import type { AsThunk, EmptyObject, LoosenThunkReturn, NoArgs, ThunkReturnFor } from '[core-types]';
7+
8+
/**
9+
* -----------------------------------------------------------
10+
* AsThunk - uses ThunkReturnFor + LoosenThunkReturn
11+
* -----------------------------------------------------------
12+
*/
13+
expectTypeOf<AsThunk<{}>>().toEqualTypeOf<() => NoArgs | [] | EmptyObject | undefined | void>();
14+
expectTypeOf<AsThunk<unknown>>().toEqualTypeOf<
15+
() => NoArgs | [] | EmptyObject | undefined | void
16+
>();
17+
expectTypeOf<AsThunk<[]>>().toEqualTypeOf<() => NoArgs | [] | EmptyObject | undefined | void>();
18+
expectTypeOf<AsThunk<{ foo: number }>>().toEqualTypeOf<
19+
() => NoArgs | [] | EmptyObject | undefined | void
20+
>();
21+
22+
expectTypeOf<AsThunk<{ named: { foo: number } }>>().toEqualTypeOf<
23+
() => { named: { foo: number } } | { foo: number }
24+
>();
25+
expectTypeOf<AsThunk<{ named: { foo: number }; positional: [string] }>>().toEqualTypeOf<
26+
() => { named: { foo: number }; positional: [string] }
27+
>();
28+
expectTypeOf<AsThunk<{ positional: [number] }>>().toEqualTypeOf<
29+
() => { positional: [number] } | [number]
30+
>();
31+
32+
/**
33+
* -----------------------------------------------------------
34+
* LoosenThunkReturn
35+
* -----------------------------------------------------------
36+
*/
37+
expectTypeOf<LoosenThunkReturn<{ named: { foo: 1 }; positional: [] }>>().toEqualTypeOf<
38+
{ foo: 1 } | { named: { foo: 1 } }
39+
>();
40+
expectTypeOf<LoosenThunkReturn<{ named: EmptyObject; positional: [string] }>>().toEqualTypeOf<
41+
[string] | { positional: [string] }
42+
>();
43+
expectTypeOf<LoosenThunkReturn<{ named: { foo: 1 }; positional: [string] }>>().toEqualTypeOf<{
44+
named: { foo: 1 };
45+
positional: [string];
46+
}>();
47+
48+
/**
49+
* -----------------------------------------------------------
50+
* ThunkReturnFor
51+
* -----------------------------------------------------------
52+
*/
53+
expectTypeOf<ThunkReturnFor<{}>>().toEqualTypeOf<NoArgs>();
54+
expectTypeOf<ThunkReturnFor<unknown>>().toEqualTypeOf<NoArgs>();
55+
expectTypeOf<ThunkReturnFor<object>>().toEqualTypeOf<NoArgs>();
56+
// How to guard against this situation?
57+
// expectTypeOf<ThunkReturnFor<Record<string, unknown>>>().toEqualTypeOf<NoArgs>();
58+
expectTypeOf<ThunkReturnFor<{ positional: [string] }>>().toEqualTypeOf<{
59+
positional: [string];
60+
named: EmptyObject;
61+
}>();
62+
expectTypeOf<ThunkReturnFor<{ Positional: [string] }>>().toEqualTypeOf<{
63+
positional: [string];
64+
named: EmptyObject;
65+
}>();
66+
expectTypeOf<ThunkReturnFor<{ named: { baz: string } }>>().toEqualTypeOf<{
67+
positional: [];
68+
named: { baz: string };
69+
}>();
70+
expectTypeOf<ThunkReturnFor<{ Named: { baz: string } }>>().toEqualTypeOf<{
71+
positional: [];
72+
named: { baz: string };
73+
}>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expectTypeOf } from 'expect-type';
2+
3+
import type { Class, Constructor } from '[core-types]';
4+
5+
class A {
6+
a = 1;
7+
}
8+
9+
/**
10+
* Class + Constructor
11+
*/
12+
expectTypeOf<InstanceType<Class<A>>>().toMatchTypeOf<A>();
13+
expectTypeOf<InstanceType<Constructor<A>>>().toMatchTypeOf<A>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
*
3+
* NOTE: these examples are explicitly for testing types and may not be
4+
* suitable for actual runtime usage
5+
*/
6+
import { expectTypeOf } from 'expect-type';
7+
8+
// import { AsThunk, Class, ContextOf, ExpandThunkReturn, Named, Positional } from '[core-types]';
9+
import { Resource } from '../resource';
10+
11+
/**
12+
* with no arguments specified
13+
*/
14+
class A extends Resource {
15+
a = 1;
16+
}
17+
18+
// TODO: rename to create when there are no args?
19+
// are there use cases for class-based Resources without args?
20+
// no args seems like it'd be easier as a function-resource.
21+
// Valid, no args present
22+
// A.from();
23+
A.from(() => ({}));
24+
A.from(() => []);
25+
26+
// Invalid, A does not expect args
27+
// @ts-expect-error
28+
A.from(() => ({ positional: [1] }));
29+
//
30+
// Invalid, A does not expect args
31+
// @ts-expect-error
32+
A.from(() => ({ named: { foo: 2 } }));
33+
34+
// valid, empty args are ok
35+
A.from(() => ({ positional: [], named: {} }));
36+
37+
export class UsageA {
38+
a = A.from(this, () => ({}));
39+
a1 = A.from(this, () => []);
40+
41+
// Invalid, A does not expect args
42+
// @ts-expect-error
43+
a2 = A.from(this, () => ({ positional: [1] }));
44+
45+
// Invalid, A does not expect args
46+
// @ts-expect-error
47+
a3 = A.from(this, () => ({ named: { foo: 2 } }));
48+
49+
// valid, empty args are ok
50+
a4 = A.from(this, () => ({ positional: [], named: {} }));
51+
}
52+
53+
/**
54+
* with all arguments specified
55+
*/
56+
type BArgs = {
57+
positional: [num: number, greeting: string];
58+
named: {
59+
num: number;
60+
str: string;
61+
};
62+
};
63+
64+
export class B extends Resource<BArgs> {
65+
b = 'b';
66+
}
67+
68+
// Valid, all arguments provided
69+
B.from(() => {
70+
return {
71+
positional: [1, 'hi'],
72+
named: { num: 2, str: 'there' },
73+
};
74+
});
75+
76+
export class UsageB {
77+
// everything missing
78+
// @ts-expect-error
79+
b = B.from(this, () => ({}));
80+
81+
// named is missing
82+
// @ts-expect-error
83+
b1 = B.from(this, () => ({ positional: [1, 'hi'] }));
84+
85+
// positional is missing
86+
// @ts-expect-error
87+
b2 = B.from(this, () => ({ named: { num: 2, str: 'there' } }));
88+
89+
// positional is incorrect
90+
// @ts-expect-error
91+
b3 = B.from(this, () => ({ positional: ['hi'] }));
92+
93+
// named is incorrect
94+
// @ts-expect-error
95+
b4 = B.from(this, () => ({ named: { str: 'there' } }));
96+
97+
// valid -- all args present
98+
b5 = B.from(this, () => ({ positional: [1, 'hi'], named: { num: 2, str: 'there' } }));
99+
}
100+
101+
/**
102+
* with all arguments, but capitalized (Signature style)
103+
*/
104+
105+
type CArgs = {
106+
Positional: [number, string];
107+
Named: {
108+
num: number;
109+
str: string;
110+
};
111+
};
112+
113+
export class C extends Resource<CArgs> {
114+
c = 'c';
115+
}
116+
117+
/**
118+
* The return value of the thunk has the correct type
119+
*/
120+
export class UsageC {
121+
// decorator not needed for the type test (I don't want to import it)
122+
/* @use */ cUse = C.from(() => ({ positional: [1, 'two'], named: { num: 3, str: 'four' } }));
123+
cThis = C.from(this, () => ({ positional: [1, 'two'], named: { num: 3, str: 'four' } }));
124+
}
125+
126+
expectTypeOf(new UsageC().cUse).toEqualTypeOf<C>();
127+
expectTypeOf(new UsageC().cThis).toEqualTypeOf<C>();

0 commit comments

Comments
 (0)