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.
>Solution :
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