Advanced Mixins on Typescript

Mixins on Typescript

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 Animals, 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.

Published 28 Mar 2019

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter