Skip to content

Commit f8345e7

Browse files
committed
Adds examples/tests of constructing generic types.
This mostly works as expected, with the one caveat that constructing a generic `Foo<T>` with `ctor.new(Foo)` actually results in a return-only generic. TypeScript is a little awkward about these and it will actually be typed as `Foo<unknown>`. This is unfortunate, and the best practice around return-only generics is generally considered casting them to the desired type at each call site. This means that a user should really do `ctor.new(Foo) as ctor<Foo<T>>`, which is a bit annoying syntatically, but not that big a deal in the grand scheme of things. More annoyingly is that TypeScript does not allow type parameters in the `extends` clause of a class when used as an expression. This makes sense for TypeScript because a class type `T` is not known until the class is instantiated, long after it was declared as extending a different class. However, with the recent changes in `ctor<T>`, the inheritance hierarchy is not built until construction-time, so we actually do know the value of `T`. Unfortunately this can't be expressed in TypeScript, so our best option is to simply `@ts-ignore` the error. This is not ideal, but TypeScript *mostly* does what we want in this case. The one issue here is that superclass members are typed as `any`, which is quite unfortunate but I don't see a good way around that. See: microsoft/TypeScript#26154 (comment)
1 parent dbba18c commit f8345e7

File tree

1 file changed

+60
-0
lines changed

1 file changed

+60
-0
lines changed

src/ctor_test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,66 @@ describe('ctor', () => {
165165
expect(bar.abstractFooMethod()).toBe('abstractFooMethod');
166166
});
167167

168+
it('supports generics', () => {
169+
class Foo<T> {
170+
public readonly value: T;
171+
172+
/** Boilerplate, should be generated by compiler. */
173+
public constructor({ value }: { value: T }) {
174+
this.value = value;
175+
}
176+
177+
public static from<T>(value: T): Foo<T> {
178+
return Foo.extend(value).construct();
179+
}
180+
181+
public static extend<T>(value: T): ctor<Foo<T>> {
182+
// Ctor has a return-only generic, so generic classes need to be
183+
// casted to the correct T value.
184+
return ctor.new(Foo, { value }) as ctor<Foo<T>>;
185+
}
186+
}
187+
188+
const foo = Foo.from('test');
189+
expect(foo).toBeInstanceOf(Foo);
190+
expect(foo.value).toBe('test');
191+
// @ts-expect-error `foo.value` should be typed as string.
192+
((x: number) => x = foo.value);
193+
194+
// @ts-ignore In TypeScript, type parameters are not in scope for
195+
// superclass declarations because type T is not known when the class
196+
// hierarchy is created. This makes sense in TypeScript, but not for
197+
// `ctor<T>`, where the class hierarchy is dynamic until it is
198+
// constructed. We simply ignore the type error for now.
199+
// See: https://github.com/microsoft/TypeScript/issues/26154#issuecomment-410848076
200+
class Bar<T1, T2> extends Implementation<Foo<T1>>() {
201+
public readonly value2: T2;
202+
203+
/** Boilerplate, should be generated by compiler. */
204+
public constructor({ value2 }: { value2: T2 }) {
205+
super();
206+
this.value2 = value2;
207+
}
208+
209+
public static from<T1, T2>(value: T1, value2: T2): Bar<T1, T2> {
210+
const barCtor = from(Foo.extend(value))
211+
.new(Bar, { value2 }) as ctor<Bar<T1, T2>>;
212+
return barCtor.construct();
213+
}
214+
}
215+
216+
const bar = Bar.from('test', 1);
217+
expect(bar).toBeInstanceOf(Foo);
218+
expect(bar).toBeInstanceOf(Bar);
219+
expect(bar.value).toBe('test');
220+
expect(bar.value2).toBe(1);
221+
// `bar.value` **should** be typed as string, but TypeScript's generic
222+
// limitations mean this is currently incorrectly typed as `any`.
223+
((x: number) => x = bar.value);
224+
// @ts-expect-error `bar.value2` should be typed as number.
225+
((x: string) => x = bar.value2);
226+
});
227+
168228
it('allows static factories of the same name across inherited classes', () => {
169229
class Foo {
170230
public readonly foo: string;

0 commit comments

Comments
 (0)