Recently I have come across Mixins in TypeScript as a way of "extending" multiple classes. However, using multiple Mixins could result in fairly unreadable nested Mixin functions:
class Dog extends Domesticated(Barking(PackHunting(Carnivore(FourLegged(Animal))))) {}
Now while this offers some flexibility and specificity, for example PackHunting being exclusive to Carivore classes, the amount of brackets does not make for clean and readable code. Quite often I don't need the order-specificity anyways and just need all my Mixins to inherit from the same Base class.
What I did was creating a Mixin function, that chains all the Mixins together, reducing the brackets to just one pair:
function mixin<T extends Constructor, M extends MixinFunction<T, any>[]>(
Base: T,
...mixins: M
): MixinReturnValue<M> {
return mixins.reduce((mix, applyMixin) => applyMixin(mix), Base) as MixinReturnValue<M>;
}
Using the helper types:
type Constructor<T = {}> = new (...args: any[]) => T;
type MixinFunction<T extends Constructor = Constructor, R extends T = T & Constructor> = (Base: T) => R;
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;
type MixinReturnValue<T extends MixinFunction<any, any>[]> = UnionToIntersection<
{ [K in keyof T]: T[K] extends MixinFunction<any, infer U> ? U : never }[number]
>;
This simplifies the Dog class to
class Dog extends mixin(Animal, FourLegged, Carnivore, PackHunting, Barking, Domesticated) {}
With all of the mixed in classes required to extending the base class Animal, e.g.:
function Barking<TBase extends Constructor<Animal>>(Base: TBase) {
return class Barking extends Base {
public bark() { console.log('woof'); }
};
}
The question implied by this post is more of a review request: Is this a sensible approach or did I miss something major in my testing? In Particular, I'm not overly happy with the use of the UnionToIntersection hack that calculates the correct return type for the Mixin function.
I did find this article going deeper into chaining mixins which depend on each other in a specific order. But for my current setup I don't really require order-dependent mixins.
Link to a playground containing some of the tests I did: TS Playground
Update
In case anyone is interested, I threw together a small package containing the approach outlined here: https://www.npmjs.com/package/ts-mixin-extended.
It does have some extra-smarts, such as MixinINstance (InstanceType for mixin functions), but is otherwise more or less the same.