Type annotations: How to inherit from a Mixin of several classes and preserve static methods?

Viewed 808

It is only the ts annotation problem, in js runtime works everything as expected.

For a multiple inheritance/mixin, we have a runtime method which takes classes/objects and creates compound(mixed) class.

class A {
    a: string
    static staticA: string
}
class B {
    b: string
    static staticB: string
}
class C extends mixin(A, B) {
    c: string
    static staticC: string
}

So our mixin method creates the mixed class which the C inherits. Now we have some annotation problems. Simple mixin declaration looks like this (actually, mixin accepts also objects for T1 and T2, but to make it simple I removed it from code):

interface Constructor<T = {}> {
    new (...args: any[]): T;    
}

declare function mixin<T1 extends Constructor, T2 extends Constructor> (
    mix1: T1, 
    mix2: T2
): new (...args) => (InstanceType<T1> & InstanceType<T2>)

Unfortunately, the type returned by mixin looses static methods for T1 and T2.

C. /* only 'staticC' is present in autocomplete */
let c = new C;
c. /* all - 'a', 'b' and 'c' are present in autocomplete */

I also tried to return the type T1 & T2, but get the error in mixin(A, B)

[ts] Base constructors must all have the same return type.

Is there any solution for this?


Final Solution

Thanks to @titian-cernicova-dragomir. I'm adding here my final solution, I have extended the annotation to support also objects, not only classes, hopefully it will be helpful for somebody.

// Extract static methods from a function (constructor)
type Statics<T> = {
    [P in keyof T]: T[P];
}

declare function mixin<
    T1 extends Constructor | object, 
    T2 extends Constructor | object,
    T3 extends Constructor | object = {},
    T4 extends Constructor | object = {},
    > (
        mix1: T1,
        mix2: T2,
        mix3?: T3,
        mix4?: T4,
    ): 
    (T1 extends Constructor ? Statics<T1> : {}) & 
    (T2 extends Constructor ? Statics<T2> : {}) &
    (T3 extends Constructor ? Statics<T3> : {}) &
    (T4 extends Constructor ? Statics<T4> : {}) &
    (new (...args: T1 extends Constructor ? ConstructorParameters<T1> : never[]) =>
        (T1 extends Constructor ? InstanceType<T1> : T1) &
        (T2 extends Constructor ? InstanceType<T2> : T2) &
        (T3 extends Constructor ? InstanceType<T3> : T3) &
        (T4 extends Constructor ? InstanceType<T4> : T4)
    );



class A {
    a: string
    static staticA: string
}
class B {
    b: string
    static staticB: string
}
const Utils = {
    log () {}
}
class C extends mixin(A, B, Utils) {
    c: string
    static staticC: string
}
C. // has 'staticA', 'staticB', 'staticC'
let c = new C;
c. // has 'a', 'b', 'c', 'log'

I have also added arguments support from the first class's constructor (if any).

...args: T1 extends Constructor ? ConstructorParameters<T1> : never[].

Unfortunately, I couldn't find the solution to make the mixin annotation to support any number of arguments, currently I made 4, as it is enough for my case. Though our js mixin can accept any number of classes/objects to create mixed class.

2 Answers

I can't see your mixin mechanism... but it looks like the original version, rather than the updated version.

The example below taken from TypeScript Mixins Part Three shows that static properties are handled in terms of the types and the runtime behaviour when you use this method of creating mixins.

Mixins with Static Members

type Constructor<T = {}> = new (...args: any[]) => T;

function Flies<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        static altitude = 100;
        fly() {
            console.log('Is it a bird? Is it a plane?');
        }
    };
}

function Climbs<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        static stickyHands = true;
        climb() {
            console.log('My spider-sense is tingling.');
        }
    };
}

class Hero {
    constructor(private name: string) {

    }
}

const HorseFlyWoman = Climbs(Flies(Hero));

const superhero = new HorseFlyWoman('Shelley');
superhero.climb();
superhero.fly();

console.log(HorseFlyWoman.stickyHands);
console.log(HorseFlyWoman.altitude);

Runnable Version

Here is the transpiled version so you can see the output:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    }
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
function Flies(Base) {
    var _a;
    return _a = /** @class */ (function (_super) {
            __extends(class_1, _super);
            function class_1() {
                return _super !== null && _super.apply(this, arguments) || this;
            }
            class_1.prototype.fly = function () {
                console.log('Is it a bird? Is it a plane?');
            };
            return class_1;
        }(Base)),
        _a.altitude = 100,
        _a;
}
function Climbs(Base) {
    var _a;
    return _a = /** @class */ (function (_super) {
            __extends(class_2, _super);
            function class_2() {
                return _super !== null && _super.apply(this, arguments) || this;
            }
            class_2.prototype.climb = function () {
                console.log('My spider-sense is tingling.');
            };
            return class_2;
        }(Base)),
        _a.stickyHands = true,
        _a;
}
var Hero = /** @class */ (function () {
    function Hero(name) {
        this.name = name;
    }
    return Hero;
}());
var HorseFlyWoman = Climbs(Flies(Hero));
var superhero = new HorseFlyWoman('Shelley');
superhero.climb();
superhero.fly();
console.log(HorseFlyWoman.stickyHands);
console.log(HorseFlyWoman.altitude);

You can keep you original mixin function as is, and just intersect the constructor you return with T1 and T2

class A {
    a!: string
    static staticA: string
}
class B {
    b!: string
    static staticB: string
}
class C extends mixin(A, B) {
    c!: string
    static staticC: string
}

interface Constructor<T = {}> {
    new(...args: any[]): T;
}

declare function mixin<T1 extends Constructor, T2 extends Constructor>(
    mix1: T1,
    mix2: T2
): {
    new(...args: any[]): (InstanceType<T1> & InstanceType<T2>)
} & T1 & T2

C.staticA
C.staticB
C.staticC
let c = new C;
c.a
c.b
c.c

Playground link

You mention you tried T1 & T2 the problem with that approach is that would not change the constructor to return (InstanceType<T1> & InstanceType<T2>). You must add this new signature to the constructor and the original classes as well.

Related