I'd like the static methods (which act as builders) defined on a base class to automatically infer a derived class and the wrapped type. I have been able to infer one or the other, but not both. A simplified example and a playground to fiddle with:
export class Collection<T> {
constructor(private _xs: T[]) {}
static fromArray1<C extends typeof Collection, T>(this: C, xs: T[]): InstanceType<C> {
return new this(xs) as InstanceType<C>;
}
static fromArray2<C extends Collection<T>, T>(this: new (xs: T[]) => C, xs: T[]): C {
return new this(xs);
}
}
export class ExtendedCollection<T> extends Collection<T> {}
// Goal: automatically infer as ExtendedCollection<number>
const ec1 = ExtendedCollection.fromArray1([1, 2, 3]);
const ec2 = ExtendedCollection.fromArray2([1, 2, 3]);
CodePudding user response:
The major impediment to your goal is the absence of direct support for higher kinded types in TypeScript, of the sort requested in microsoft/TypeScript#1213. Right now you can build generic types like type Gen<T extends Foo> = ...
where the type parameter T
represents some specific type, but you can't abstract over that to make a higher generic type like type Higher<F<~> extends Gen<~>> = ...
where the type parameter F
represents some generic type which itself takes a type argument like Gen
.
If you could, then maybe you would be able to write
// NOT VALID TS, DON'T TRY THIS:
static fromArray<C<~> extends Collection<~>, T>(
this: new (...args: any) => C<any>,
xs: T[]
): C<T>;
and "apply" the generic type parameter C
to the type T
to produce C<T>
. But this is not directly supported, so we have to work around it.
The issue, microsoft/TypeScript#1213, mentions several flavors of workaround. All of them currently require some boilerplate code that needs to be maintained. For example, if you are willing to merge a property into an interface for every defined subclass of Collection<T>
, you can define an "apply"-like type function:
export class Collection<T> {
constructor(private _xs: T[]) { }
static fromArray<C extends typeof Collection, T>(this: C, xs: T[]): Apply<C, T> {
return new this(xs) as any;
}
x = 0
}
interface HKT<T> { Collection: Collection<T> }
type Apply<C extends new (...args: any) => any, T> =
{ [K in keyof HKT<any>]:
HKT<any>[K] extends InstanceType<C> ?
InstanceType<C> extends HKT<any>[K] ?
HKT<T>[K] : never : never }[keyof HKT<any>]
From the above, you could figure out that Apply<typeof Collection, number>
will produce Collection<number>
. Then for subclasses, you merge in another property:
export class ExtendedCollection<T> extends Collection<T> {
y = 1
}
// manually merge in this
interface HKT<T> { ExtendedCollection: ExtendedCollection<T> }
And now, Apply<typeof ExtendedCollection, string>
will produce ExtendedCollection<number>
. So your example will work as desired:
const ec1 = ExtendedCollection.fromArray([1, 2, 3]);
// const ec1: ExtendedCollection<number>
ec1.y
Without trying to simulate higher kinded types, the only other workaround I can think of is to just give up on abstracting "apply C
to T
" over all C
, and instead just manually narrow each subclass so that its methods have hardcoded references to themselves:
export class Collection<T> {
constructor(private _xs: T[]) { }
static fromArray<T>(xs: T[]): Collection<T> {
return new this(xs) as any;
}
x = 0
}
So that's the base class, and then subclasses will just mark their method types appropriately without actually modifying them:
export class ExtendedCollection<T> extends Collection<T> {
static fromArray: <T>(xs: T[]) => ExtendedCollection<T>;
y = 1;
}
And again, the example works as desired:
const ec1 = ExtendedCollection.fromArray([1, 2, 3]);
// const ec1: ExtendedCollection<number>
ec1.y
Neither method is perfect, obviously. There may be other workarounds that fit your needs more closely, but until and unless microsoft/TypeScript#1213 is implemented, I don't think there are any completely pain-free ways of doing it.