Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 458
Kiikurage Avatar asked Jan 21 '18 22:01

Kiikurage


People also ask

What is mixin in TypeScript?

Mixins are a faux-multiple inheritance pattern for classes in JavaScript which TypeScript has support for. The pattern allows you to create a class which is a merge of many classes. To get started, we need a type which we'll use to extend other classes from.

How do you use a decorator in TypeScript?

Using Decorator Syntax In TypeScript, you can create decorators using the special syntax @expression , where expression is a function that will be called automatically during runtime with details about the target of the decorator. The target of a decorator depends on where you add them.

What is true about mixins in TypeScript?

Therefore, TypeScript provides mixins that help to inherit or extend from more than one class, and these mixins create partial classes and combine multiple classes to form a single class that inherits all the functionalities and properties from these partial classes.

Are TypeScript decorators experimental?

Since decorators are an experimental feature, they are disabled by default. You must enable them by either enabling it in the tsconfig. json or passing it to the TypeScript compiler ( tsc ).


1 Answers

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!

like image 183
jcalz Avatar answered Sep 22 '22 13:09

jcalz