Search code examples
typescriptgenericsdecoratormixins

Type-safe mixin decorator in TypeScript


I tried to define type-safe mixin() decorator function like follows,

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

function mixin<T>(MixIn: Constructor<T>) {
    return function decorator<U>(Base: Constructor<U>) : Constructor<T & U> {
        Object.getOwnPropertyNames(MixIn.prototype).forEach(name => {
            Base.prototype[name] = MixIn.prototype[name];
        });

        return Base as Constructor<T & U>;
    }
}

And used it as follows,

class MixInClass {
    mixinMethod() {console.log('mixin method is called')}
}

/**
 *  apply mixin(MixInClass) implicitly (use decorator syntax)
 */
@mixin(MixInClass)
class Base1 {
    baseMethod1() { }
}
const m1 = new Base1();
m1.baseMethod1();
m1.mixinMethod(); // error TS2339: Property 'mixinMethod' does not exist on type 'Base1'.

Then, compiler said m1 didn't have the member 'mixinMethod'.

And generated code is as follows,

//...
var Base1 = /** @class */ (function () {
    function Base1() {
    }
    Base1.prototype.baseMethod1 = function () { };
    Base1 = __decorate([
        mixin(MixInClass)
    ], Base1);
    return Base1;
}());
//...

It looks that mixin decorator was applied correctly.

So, in my understanding, the type of m1 is inferred as Base1 & MixIn. But compiler says it's just Base1.

I used tsc 2.6.2 and compiled these codes with --experimentalDecorators flag.

Why does compiler fail to recognize the type as I expected?


Based on @jcalz's answer, I modified my code as follows,

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

function mixin<T1, T2>(MixIns:  [Constructor<T1>, Constructor<T2>]): Constructor<T1&T2>;
function mixin(MixIns) {
    class Class{ };

    for (const MixIn of MixIns) {
        Object.getOwnPropertyNames(MixIn.prototype).forEach(name => {
            Class.prototype[name] = MixIn.prototype[name];
        });
    }

    return Class;
}

class MixInClass1 {
    mixinMethod1() {}
}

class MixInClass2 {
    mixinMethod2() {}
}

class Base extends mixin([MixInClass1, MixInClass2]) {
    baseMethod() { }
}

const x = new Base();

x.baseMethod(); // OK
x.mixinMethod1(); // OK
x.mixinMethod2(); // OK
x.mixinMethod3(); // Property 'mixinMethod3' does not exist on type 'Base' (Expected behavior, Type check works correctly)

This works pretty well. I want to improve this mixin function for variable length mixin classes.

One solution is adding overload function declaration like follows,

function mixin<T1>(MixIns: [Constructor<T1>]): Constructor<T1>;
function mixin<T1, T2>(MixIns: [Constructor<T1>, Constructor<T2>]): Constructor<T1&T2>;
function mixin<T1, T2, T3>(MixIns: [Constructor<T1>, Constructor<T2>, Constructor<T3>]): Constructor<T1&T2&T3>;

But this is too ugly. Are there any good ideas? Is it impossible until variadic-kind is supported?


Solution

  • Decorators don't mutate the type signature of the decorated class the way you're expecting. There's a rather lengthy issue in Github which discusses this, and it's not clear there's agreement on how (or if) such mutation should be implemented. The main problem right now is that the compiler understands Base1 as the undecorated class, and doesn't have a name for the decorated version.

    From reading that Github issue, it looks like the suggested workaround (for now at least) is something like:

    class Base1 extends mixin(MixInClass)(
      class {
        baseMethod1() { }
      }) {
    }
    

    So you're not using the decorator @ notation, and instead directly applying the decorator function to an anonymous class (which has the same implementation of your desired Base1), and then subclassing that to get Base1. Now the compiler understands that Base1 has both a baseMethod1() and a mixinMethod().

    Hope you find that helpful. Good luck!