TypeScript 2.2 first introduced the support for mixins to huge applause from the developers. Mixins pattern allows you to abstract code into separate chunks and DRY code is always better, right? A very simple example of a mixin from the official TypeScript docs:
class Point {
constructor(public x: number, public y: number) {}
}
class Person {
constructor(public name: string) {}
}
type Constructor<T> = new(...args: any[]) => T;
function Tagged<T extends Constructor<{}>>(Base: T) {
return class extends Base {
_tag: string;
constructor(...args: any[]) {
super(...args);
this._tag = "";
}
}
}
const TaggedPoint = Tagged(Point);
let point = new TaggedPoint(10, 20);
point._tag = "hello";
class Customer extends Tagged(Person) {
accountBalance: number;
}
let customer = new Customer("Joe");
customer._tag = "test";
customer.accountBalance = 0;
So in short, mixins can be used to supercharge your classes with extra attributes and methods. But what if you want to inject a common functionality to several similar classes that all implement a particular interface. For example, let’s say you have a mixin that produces some sound.
// Activatable Mixin
class Speakable {
speak() {
// Speak something
}
}
This can then be injected into a Dog
and a Cat
to make them speak. But what if the speak
method needs to call a specific method sound
on the instances? While this is possible with TypeScript, it is very hard to set up. Here is how an implementation for this would look like.
interface Animal {
sound: string;
speak: () => void;
}
type Constructor<T = Animal> = new (...args: any[]) => T;
export default function Speakable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
speak(this.sound);
};
}
class BaseDog extends Animal {
sound = 'wow';
speak() { throw new Error('Method not implemented.'); }
}
class Dog extends Speakable(BaseDog) {}
class BaseCat extends Animal {
sound = 'meow';
speak() { throw new Error('Method not implemented'); }
}
class Cat extends Speakable(BaseCat) {}
Let’s break this down. We have an interface Animal
that is supposed to have a sound
and speak
something. But since the speak
implementation is very involved and same for all Animal
s, we want to provide an abstraction for it rather than having to define it on all sub-classes of Animal
. On the contrived example, you would say let’s create a helper function that both classes can call. While that sounds ok for a small example like this, when it gets to the point of providing several functionalities that depend on one another, say speak
, walk
, run
, cry
etc. it becomes really difficult to manage it with helper functions.
The mixins approach, on the other hand, helps us compose classes as we would while allowing us to provide default implementations for the common methods into a single mixin. We could have several different mixins that can be nested one after the other to provide different independent functionalities too.
class Cat extends Taggable(Speakable(BaseCat)) {}
While this does look slightly hard to read, the benefit in terms of DRYness far outweights the small issue of aesthetics.