Home > front end >  How to explicitly annotate the return type of a TypeScript mixin factory function?
How to explicitly annotate the return type of a TypeScript mixin factory function?

Time:11-02

Given the example TypeScript mixin pattern described here:

type Constructor = new (...args: any[]) => {};
 
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
 
function Scale<TBase extends Constructor>(Base: TBase) {
  return class Scaling extends Base {
    // Mixins may not declare private/protected properties
    // however, you can use ES2020 private fields
    _scale = 1;
 
    setScale(scale: number) {
      this._scale = scale;
    }
 
    get scale(): number {
      return this._scale;
    }
  };
}

How would we explicitly annotate the return type of the Scale function? That is, fill in the ???:

function Scale<TBase extends Constructor>(Base: TBase): ??? { 
  ...

CodePudding user response:

You don't need to declare explicit return type for Scale function because TypeScript is smart enought to infer the return type. Hover your mouse on Scale and you will see that return type is

{
    new (...args: any[]): Scaling;
    prototype: Scale<any>.Scaling;
} & TBase

Further more, if you want to use explicit return type for Scale, you should declare Scaling (the inner class) out of Scale function. Like this:

 class Scaling extends Base {

    _scale = 1;
 
    setScale(scale: number) {
      this._scale = scale;
    }
 
    get scale(): number {
      return this._scale;
    }
  }

function Scale<TBase extends Constructor>(Base: TBase) {
  return Scaling
}

But then Base should be static.

It means that we should create Mixin function, just like in the docs - Alternative Pattern:

// credits goes to credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends (
        k: infer I
    ) => void
    ? I
    : never;

type ClassType = new (...args: any[]) => any;

function Mixin<T extends ClassType, R extends T[]>(...classRefs: [...R]):
    new (...args: any[]) => UnionToIntersection<InstanceType<[...R][number]>> {
    return merge(class { }, ...classRefs);
}

function merge(derived: ClassType, ...classRefs: ClassType[]) {
    classRefs.forEach(classRef => {
        Object.getOwnPropertyNames(classRef.prototype).forEach(name => {
            // you can get rid of type casting in this way
            const descriptor = Object.getOwnPropertyDescriptor(classRef.prototype, name)
            if (name !== 'constructor' && descriptor) {
                Object.defineProperty(
                    derived.prototype,
                    name,
                    descriptor
                );
            }
        });
    });

    return derived;
}

class Foo {
    tag = 'foo'
}

class Scaling extends Mixin(Foo) {

    _scale = 1;

    setScale(scale: number) {
        this._scale = scale;
    }

    get scale(): number {
        return this._scale;
    }
}

const result = new Scaling();

result.tag // string
result.scale // number

You can find my article in medium and in my blog step-by-step explanation.

UnionToIntersection - creates intersection of union type. Full explanation you can find in this answer

ClassType - almost the same type as your Constructor. It is a type of any class constructor.

Mixin - infers with help of variadic tuple types each class constructor provided in arguments and merges all their instances into one object UnionToIntersection<InstanceType<[...R][number]>>.

`[...R][number]` - takes a union of all provided class intstances
`InstanceType<[...R][number]>>` - replace every class constructor in the union 
                                  with class instance accordingly
`UnionToIntersection<InstanceType<[...R][number]>>` - merges all class instances

merge - is the same as applyMixins from the docs

UPDATE

Since, Scaling is defined inside a function, it is impossible to add explicit return type which includes Scaling because it just not exists in the scope yet.

  • Related