Make mixin with generic type - phase 2

45 Views Asked by At

I would like to define generic type for my mixin method that uses "extending class creating functions". But I am unable to define the good syntax in order to do it.

I took the implementation in this answer https://stackoverflow.com/a/76585028/762461

Here is a short and simple example :

// The type for the so called "extending class creating functions"
type Mixable<A extends any[], R> = (
  ctor: new (...args: any[]) => any
) => abstract new (...args: A) => R;

type MixinFunction = {
  <A1 extends any[], R1>(ctor1: Mixable<A1, R1>): new (...args: any) => R1;
  <A1 extends any[], R1, A2 extends any[], R2>
  (ctor1: Mixable<A1, R1>, ctor2: Mixable<A2, R2>): new (...args: any) => R1 & R2;
}

const Mixin: MixinFunction = ((_a: Mixable<any[], any>) => {
  // this content is only for the example
  return class {} as (new (...args: any[]) => any);
});

const Base = (superclass: Newable) => class _Base extends superclass {
  foo():number {
    return 1;
  }
};

const Base2 = <T>(superclass: Newable) => class _Base2 extends superclass {
  bar():T {
    return 1 as T;
  }
};

// class Derived<T> extends Mixin(Base, Base2<T>) doesn't work because of ts(2562)
class Derived<T> extends Mixin(Base, Base2) {
}

class DerivedString extends Derived<string> {}

(new Derived()).foo();          // infered as "number"
(new DerivedString()).bar();    // I would like to infer the return type here as "string"
1

There are 1 best solutions below

1
jcalz On

Unfortunately TypeScript lacks features necessary for you to write this sort of composed generic mixin. The support for higher order type inference from generic functions only works in very specific circumstances, and using Base2 (a generic function that returns a nongeneric class) as a Mixable (a generic type that returns a nongeneric function that returns a nongeneric class) isn't one of those circumstances.

To begin to support this sort of thing TypeScript would probably need, at the minimum, a loosening of the restriction of accessing type parameters in base class expressions as requested in microsoft/TypeScript#55972. But in general TypeScript can't really express the sort of higher-kinded types that represent the intended operation of Mixin when applied to generic inputs. There's a feature request at microsoft/TypeScript#1213 for that, although again, it's not clear that an implementation for that would automatically fix your issue.

For now, this is just beyond TypeScript's abilities.


Instead of trying to get TypeScript to do what it can't, the best I can imagine doing is to figure out what type you expect Mixin(Base, Base2<T>) to produce, and then just assert that the result is of that type. It's not great, but it's at least possible:

const Mixed = Mixin(Base, Base2) as new <T>(...args: any) =>
    InstanceType<ReturnType<typeof Base>> &
    InstanceType<ReturnType<typeof Base2<T>>>;
class Derived<T> extends Mixed<T> { }

class DerivedString extends Derived<string> { }
(new Derived()).foo(); // number
(new DerivedString()).bar(); // string

That's complicated, mostly because Base and Base2 have hidden away their actual classes inside the function body scope, so there's no existing name for their instance types. I use the ReturnType and the InstanceType utility types to tease that out. The generics are handled by making Mixed a generic class constructor (that's the <T> in new <T>(...args: any) => ⋯) and then using an instantiation expression to specify T as the type argument for typeof Base2.

But it works; Mixed is seen as a generic class so you're allowed to write class Derived<T> extends Mixed<T> {}, and thus DerivedString has a string return type for bar(), as desired.

Playground link to code