Search code examples
typescriptgenericsmixins

Passing generic types through to mixins in Typescript


I'm trying to make generically typed mixins in Typescript. I realize that as of dev version 2.8.0, Typescript does not yet directly support this, per issue #13979. I'm looking for a workaround. I don't care how ugly the mixins themselves are, as long as applying mixins is clean and readable.

I need decorator support in some mixins, so I'm defining the mixins as nested classes, modeled after this helpful SO answer. The more common approach returns class expressions instead. I have not yet notice losing any functionality in the nested-class approach.

I specifically want a generic type specified in an outer mixin to determine the types found in more nested mixins, at least as viewed outside the outer mixin. I realize that a more drastically different approach may be needed to deal with the possibility that Typescript might not be able to pass types defined in outer functions down through nested function calls.

Given the following code:

interface ConcreteClass<C> { new (...args: any[]): C; }

class Entity {
    name = "base";
}

class Broker<E extends Entity> {
    static table = "entities";
}

interface BrokerType<E extends Entity> extends ConcreteClass<Broker<E>> {
    table: string;
}

class IdEntity extends Entity {
    id = 1;
}

function IdMixin<E extends IdEntity, B extends BrokerType<E>>(Base: B) {
    class AsIdBroker extends Base {
        constructor(...args: any[]) {
            super(...args);
        }
        getEntity(id: number): E | null {
            return null;
        }
    }
    return AsIdBroker;
}

class TaggedEntity extends IdEntity {
    tag = "gotcha";
}

Here's the ideal way to do it, but this doesn't work:

// OPTION A -- broken

class TaggedBroker extends Broker<TaggedEntity> { };

class MyTaggedBroker extends IdMixin(TaggedBroker) {
    myStuff = "piled";
    showTag(id: number) {
        const entity = this.getEntity(id);
        if (entity) {
            // ERROR: Property 'tag' does not exist on type 'IdEntity'.
            console.log(entity.tag);
        }
    }
}

I'd also be very happy to do it this way, but this doesn't work either:

// OPTION B -- broken

class TaggedBroker extends Broker<TaggedEntity> { };

// ERROR: Expected 2 type arguments, but got 1.
class MyTaggedBroker extends IdMixin<TaggedEntity>(TaggedBroker) {
    myStuff = "all mine";
    showTag(id: number) {
        const entity = this.getEntity(id);
        if (entity) {
            console.log(entity.tag);
        }
    }
}

This final approach makes all of the present code work, but aside from being verbose (at least with the names in my app), it doesn't carry forward the base broker's types and thus isn't really a mixin:

// OPTION C -- works here, but drops types for any previously mixed-in brokers

class TaggedBroker extends Broker<TaggedEntity> { };

class MyTaggedBroker extends IdMixin<TaggedEntity, BrokerType<TaggedEntity>>(TaggedBroker) {
    myStuff = "all mine";
    showTag(id: number) {
        const entity = this.getEntity(id);
        if (entity) {
            console.log(entity.tag);
        }
    }
}

I'm posting here in case someone knows what I need to be doing. Meanwhile, I'll keep exploring my options, including possibly an IoC approach typing nested-most mixins first.


Solution

  • To enable option C to inherit members form TaggedBroker to MyTaggedBroker you just need one simple change:

    class TaggedBroker extends Broker<TaggedEntity> { 
        public x: number;
    };
    
    class MyTaggedBroker extends IdMixin<TaggedEntity, typeof TaggedBroker>(TaggedBroker) {
        myStuff = "all mine";
        showTag(id: number) {
            console.log(this.x);
            const entity = this.getEntity(id);
            if (entity) {
                console.log(entity.tag);
            }
        }
    }
    

    A more terse usage of the IdMixin could be achieved using a two call approach, where E is specified explicitly in the first call, and B is inferred from parameter value on the second call:

    function IdMixin<E extends IdEntity>()  {
        return function <B extends BrokerType<E>>(Base: B){
            class AsIdBroker extends Base {
                constructor(...args: any[]) {
                    super(...args);
                }
                getEntity(id: number): E | null {
                    return null;
                }
            }
            return AsIdBroker;
        }
    }
    //Usage
    class MyTaggedBroker extends IdMixin<TaggedEntity>()(TaggedBroker) {
        // Same as before
    }