Home > OS >  Automatic inference of static methods for derived wrapper classes
Automatic inference of static methods for derived wrapper classes

Time:10-31

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.

Playground link to code

  • Related