Comprehensible Chaining of Mixins for a TypeScript Class without nesting

Viewed 261

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.

0 Answers
Related