Home > OS >  Do TypeScript mixins not support properties?
Do TypeScript mixins not support properties?

Time:01-11

I was attempting today to make a complicated class hierarchy that would normally require multiple inheritance in certain other languages, but with mixins in TypeScript, I thought I had a fairly straightforward way of modeling it — until I arrived at the compiler being highly unhappy at everything to do with properties inside mixins.

I started with code that looked like this:

interface IFoo {
    get foo(): boolean;
    set foo(value: boolean);
}

class FooBase implements IFoo {
    get foo(): boolean { return true; }
    set foo(value: boolean) { }
}

— which is fine. I then attempted to turn FooBase into a mixin to pull it out of the inheritance chain:

function FooMixin<T>(base: T): IFoo & T {
    return class FooMixin extends T implements IFoo {
        get foo(): boolean { return true; }
        set foo(value: boolean) { }
    }
}

The body of the class is identical; only the declaration changed slightly. But the compiler really doesn't like this mixin, complaining that

Type 'typeof FooMixin' is not assignable to type 'IFoo & T'.
  Property 'foo' is missing in type 'typeof FooMixin' but required in type 'IFoo'.

I stripped it down even further, and still got the same result:

function FooMixin(): IFoo {
    return class extends T implements IFoo {
        get foo(): boolean { return true; }
        set foo(value: boolean) { }
    }
}
Property 'foo' is missing in type 'typeof (Anonymous class)' but required in type 'IFoo'.

Clearly, it knew that foo existed as a property when the class wasn't a mixin, but the type prover seems not to have realized that get foo() exists on the mixin class at all.

This seems like a minimal, complete, verifiable example; so am I doing something really wrong here, or is the TypeScript compiler buggy around mixins and properties?

I'm using TypeScript 4.9.4, which appears to be the latest version as of this writing.

CodePudding user response:

Tell TS that T must be a constructor, so that you can then extend base. You don't need to annotate the return type as TS can infer it for you (and it's indeed very complicated):

interface IFoo {
    get foo(): boolean;
    set foo(value: boolean);
}

function FooMixin<T extends new (...args: any[]) => any>(base: T) {
    return class FooMixin extends base implements IFoo {
        get foo(): boolean { return true; }
        set foo(value: boolean) { }
    }
}

class Bar {
    bar = 0
}

const FooBar = FooMixin(Bar);

new FooBar().foo // ok
new FooBar().bar // ok

Playground

  • Related