A mixin is an abstract subclass; i.e. a subclass definition that may be applied to different superclasses to create a related family of modified classes.
(Gilad Bracha and William Cook, Mixin-based Inheritance)
How mixins can be implemented in TypeScript:
We introduce 2 methods to implement mixins in TypeScript
Method 1: Use subclass factory
Example 1:
type Contructor = new(...args:any[])=>{} function mixin1<T extends Contructor,U extends Contructor>(Base: T, Mix: U) { var SubClass = class extends Base { } Object.assign(SubClass,Mix) Object.assign(SubClass.prototype,Mix.prototype) return SubClass } class A{ static A1=1 toString(){ return 'A' } } class B{ static B1=2 toString(){ return 'B' } doJob(){ console.log('B do job') } } var C = mixin1(A,B) var c = new C() console.log(C.A1,C.B1) c.doJob()
Result:
1 2 B do job
Example 2: use Mixin with class decorator
function mixin2<U extends Contructor>(Mix: U) { return function<T extends Contructor>(Base: T){ var SubClass = class extends Base { } Object.assign(SubClass,Mix) Object.assign(SubClass.prototype,Mix.prototype) return SubClass } } @mixin2(B) class A2{ static A1=1 toString(){ return 'A' } } var a2 = new A2() console.log(a2.toString()) a2.doJob()
Result:
B B do job
Note: Mixins doesn’t support public properties, see Example 2a about this problem, and see Example 2b for fix this problem
Example 2a:
class B3{ static B3=1 propB3=1 toString(){ return 'B3' } } @mixin2(B3) class A6{ static A1=1 prop=1 toString(){ return 'A' } } var a6 = new A6() console.log(a6.propB3) //error TS2551: Property 'propB3' does not exist on type 'A6'.
Example 2b: Use intersection types and assert type
class A7{ static A7=1 propA7=1 toString(){ return 'A7' } } interface B4{ propB4:number } type MixinA7B4 = typeof A7 & B4 const instanceMixinA7B4 = (new A7() as unknown) as MixinA7B4 instanceMixinA7B4.propB4
Example 3: Multiple Mixins and Class decorator
class B2{ static B2=3 toString(){ return 'B2' } doJob(){ console.log('B2 do job') } } @mixin2(B2) @mixin2(B) class A3{ static A1=1 toString(){ return 'A' } } var a3 = new A3() console.log(a3.toString()) a3.doJob()
Result:
B2 B2 do job
Example 4: Multiple mixins
class A4 extends mixin2(B2)(B){ static A1=1 toString(){ return 'A' } } var a4 = new A4() console.log(a4.toString()) a4.doJob()
Result:
A B2 do job
Method 2: Use mixin builder
Example 5:
class MixinBuilder { superclass:any constructor(superclass:any) { this.superclass = superclass } mixin<T extends Contructor,U extends Contructor>(Base: T, Mix: U) { var SubClass = class extends Base { } Object.assign(SubClass,Mix) Object.assign(SubClass.prototype,Mix.prototype) return SubClass } mixins(...classes:any[]) { return classes.reduce((a, b) => this.mixin(a, b), this.superclass) } } let mix = (superclass:any) => new MixinBuilder(superclass) class A5 extends mix(B).mixins(B2){ static A1=1 toString(){ return 'A' } } var a5 = new A5() console.log(a5.toString()) a5.doJob()
Result:
A B2 do job
Constrained Mixins
In these above examples , we define a type:
type Contructor = new(...args:any[])=>{}
It’s equal to
interface IContructor{ new(...args:any[]):{} }
or
interface IContructor2{ new:(...args:any[])=>{} }
or
type Contructor2 = { new(...args:any[]):{} }
or
type Contructor3 = { new:(...args:any[])=>{} }
We can use a generic version which can apply a constraint on the class which this mixin is applied to
type GConstructor<T> = new (...args: any[]) => T
Now, we can create some type to work with contrained base classes use with mixins:
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }> type Spritable = GConstructor<typeof Sprite> type Loggable = GConstructor<{ print: () => void }>