Nickolay Platonov
12 March 2019

The mixin pattern in TypeScript – all you need to know

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. In this blog post, we will deep dive into the […]

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. In this blog post, we will deep dive into the mixin pattern in TypeScript that we use heavily at Bryntum for development of our products. It allows us to write very clean code and it is very similar to “typeclasses” in Haskell, “traits” in Rust and other similar “high-end” code structuring abstractions.

For the impatient and for future references we’ll provide a summary right away. Scroll below for the gentle introduction.

Cheat sheet

// mixin function
export const MyMixin =
    <T extends AnyConstructor<AlreadyImplements & BaseClass>>(base : T) =>

// internal mixin class
class MyMixin extends base {
    someProperty : string = 'initialValue'

    someMethodFromAlreadyImplementsMixin (arg : number) {
        const res = super.someMethodFromAlreadyImplementsMixin(arg)
        // ...
        return res + 1
    }

    someMethodToBeImplementedInTheConsumingClass () : number {
        throw new Error('Abstract method called')
    }

    someNewMethod () {
        if (this.someMethodToBeImplementedInTheConsumingClass() === 42) {
            this.methodFromTheBaseClass()
        }
    }
}

// the "instance type" of this mixin
export type MyMixin = Mixin<typeof MyMixin>
// or, alternatively (see the `Recursive types problem` section below for details)
export interface MyMixin extends Mixin<typeof MyMixin> {}

// "minimal" class builder
export const BuildMinimalMyMixin = 
    (base : typeof BaseClass = BaseClass) : AnyConstructor<MyMixin> =>
        AlreadyImplements(
            base
        )

export class MinimalMyMixin extends BuildMinimalMyMixin() {}
// or, alternatively
export const MinimalMyMixin = BuildMinimalMyMixin()
export type MinimalMyMixin = InstanceType<typeof MinimalMyMixin>

Supporting definitions:

export type AnyFunction<A = any> = (...input: any[]) => A
export type AnyConstructor<A = object> = new (...input: any[]) => A

export type Mixin<T extends AnyFunction> = InstanceType<ReturnType<T>>

The full and up-to-date version of supporting definitions can be found here: https://github.com/bryntum/chronograph/blob/master/src/class/Mixin.ts

Why mixin?

So what is wrong with old good “classes”? The main limitation of the classic class pattern is that it normally allows only a single super class. But the real world is not structured like that. In the real world, seemingly unrelated entities can easily share behaviour. For example, the same physic and aerodynamic laws defines the behaviour of some entity with wings. However, with a single super class you will have a hard time trying to isolate such behaviour into a reusable class, and then describing it as an aircraft or a bird.

Mixins solve exactly this problem and the hypothetical mixin “Winged” (or “HasWings”) can be easily applied to (or “mixed in”, or “consumed by”) any class.

From a mathematical point of view, one can say that the classic, single super-class inheritance creates a tree.

Tree

And mixin pattern creates a directed acyclic graph.

 

As you can imagine, arbitrary mixing of different kinds of behaviour can quickly turn into a mess. But the static typing in TypeScript prevents this problem completely, and behaviour mixing becomes very predictable and type-safe. The compiler warns you about any inconsistencies in your mixin code.

Prior art

Surprisingly, the official TypeScript documentation contains a very outdated introduction to mixins. It uses an ad-hoc copying of properties from prototype to prototype, which does not allow proper composition of methods (calling super won’t work). We do not recommend using this pattern.

There is a much better introduction by Marius Schulz, as part of his excellent “TypeScript Evolution” series. It uses the computed base class expression, allows proper method composition and is completely type-safe. Please read this article, as our post is based on the same technique with some improvements for additional type-safety.

Mixin pattern

Now, let’s go through the summary mixin definition from above in details. Let’s write it a bit differently for clarity:

export const MyMixinFunction = 
    <T extends AnyConstructor<AlreadyImplements>>(base : T) => 
{
    class MyMixinClass extends base {
    }

    return MyMixinClass
}

export type MyMixinType = Mixin<typeof MyMixinFunction>
The Mixin function

The Mixin function (MyMixinFunction) is a function that accepts a constructor of the base class (base argument), creates a subclass of that base class (MyMixinClass) and returns it. Here we did not use the compact arrow function notation and instead went for the full one, using {} brackets and an explicit return statement.

The type of the base argument is T extends AnyConstructor<AlreadyImplements> which should be read as – “any constructor function of a class that already implements (or “has consumed”) the AlreadyImplements mixin. We could specify several such requirements using &: AnyConstructor<AlreadyImplements1 & AlreadyImplements2>. If there are no requirements, we should use the object type, indicating any non-primitive type.

The Mixin class

The name MyMixinClass won’t be available outside of the mixin function – it’s like a local variable inside it. We could omit it completely, but it’s useful to have it for debugging purposes.

All the properties and methods from the AlreadyImplements1 and AlreadyImplements2 mixins will be available in the MyMixinClass class. You can override those and call the super method when / if needed. One can also define new properties and methods or upgrade the type of the inherited properties. You can also define an abstract method – a method which the consuming class must override. Please refer to the summary in the beginning of the post for examples.

The Mixin instance type

The Mixin instance type MyMixinType is an abstract type, indicating an instance of any class that has consumed this mixin. None of the actual values in the code will have this type. But the actual values will have types that extends this type.

MyMixinType is constructed with the Mixin type alias. If you look at its definition in turn, you’ll see that MyMixinType is defined as an instance type of the mixin class returned from the mixin function, which totally makes sense. We get the type of the mixin function with the typeof built-in.

There’s also an alternative notation for the mixin instance type, using interfaces, which solves the problem with recursive type definitions (see below).

Now when having a mixin instance type, we can define a function, that expects an instance of the MyMixinType as an argument:

const someFunction = (a : MyMixinType) : boolean => {
    return a.quantity > 0 ? true : false
}

Here we declare that someFunction, as its first argument, expects any object instance that implements MyMixinType. When using the first argument we restrict ourselves only to properties and methods available in the MyMixinType. The example above will immediately cause a compilation error:

TS2339: Property 'quantity' does not exist on type 'MyMixinFunction<AnyConstructor<AlreadyImplements<AnyConstructor<BaseClass>>.AlreadyImplements & BaseClass>>.MyMixinClass & AlreadyImplements<AnyConstructor<BaseClass>>.AlreadyImplements & BaseClass'

This is because the quantity property is not defined neither in our MyMixinType nor in the AlreadyImplements. This error demonstrates how TypeScript prevents us from using arbitrary properties / methods on a variable with the MyMixinType type.

All together

Considering that TypeScript has different namespaces for values and types, along with the fact that the name of the mixin class (MyMixinClass) is not available outside of the mixin function, we can just use the same name for everything – MyMixin. Such naming creates less cognitive overhead. Most of the time when referencing the name MyMixin we’ll be meaning the mixin instance type (MyMixinType).

If we use the compact notation for the arrow function, we can remove the {} brackets and return statement. Now we can write the compact notation for the mixin pattern as follows:

export const MyMixin = <T extends AnyConstructor<object>>(base : T) =>

class MyMixin extends base {
}

export type MyMixin = Mixin<typeof MyMixin>
Minimal class builder

We found it very useful to define a “minimal class builder” function for most of our mixins. This is a function that builds the smallest possible class that implements this mixin. Obviously, such a class will only contain the required “super” mixins and our mixin itself. The number of “super” mixins can be quite big, and it is convenient to write all of them on separate line. In such a “builder” function, it is also convenient to specify the default base class as the default argument for the function. If there’s no default base class, you can choose Object:

// "minimal" class builder
export const BuildMinimalMyMixin = 
    (base : typeof BaseClass = BaseClass) : AnyConstructor<MyMixin> => 
        AlreadyImplements1(    
        AlreadyImplements2(
        AlreadyImplements3(
            base
        )))

Now, if we want to apply the mixin to some base class, we just call the builder function.

Important. We found that the minimal class builder function should always have a specified return type. Otherwise TypeScript infers the type automatically, but it seems the inferred type is overly complex – the compilation time increases unacceptably. Specifying the return type manually however fixes this problem.

a Minimal class

We also found it useful to define a “minimal” class for every mixin. This is just a result from the “minimal class builder” in the previous section. It can be declared in two forms – either as a class declaration

export class MinimalMyMixin extends BuildMinimalMyMixin() {}

or as a constant. In the latter case, we also need to create a type for this constant to be able to use it in other places:

export const MinimalMyMixin = BuildMinimalMyMixin()
export type MinimalMyMixin = InstanceType<typeof MinimalMyMixin>

Type safety

The presented mixin pattern is not new and is already used in the JavaScript world. However, because of its fully dynamic nature, JavaScript can not provide the type safety of TypeScript. Because of that, the abstractions can very easily leak from one mixin to another (since the final class is usually built from several mixins). Also, programmers sometimes use the mixin pattern mechanically, just to reduce the file size by moving some of the methods from a big class to an external file containing the mixin.

TypeScript delivers much more, its static typification allows us to define a precisely encapsulated behaviour and compose it in a type safe way.

To illustrate, let’s try to “emulate” some base Haskell typeclasses with mixins. We can start with Eq:

export const Eq = <T extends AnyConstructor<object>>(base : T) =>

class Eq extends base {
    equal (another : this) : boolean {
        return !this.notEqual(another)
    }

    notEqual (another : this) : boolean {
        return !this.equal(another)
    }
}

export type Eq = Mixin<typeof Eq>

Note, how equal and notEqual method are recursively defined through each other. The consuming side only needs to define one of them and gets another one for free!

Then it goes Ord. It requires the consuming class to already implement Eq and define one of  lessOrEqual or compare.

export enum Ordering {
    LT,
    EQ,
    GT
}

export const Ord = <T extends AnyConstructor<Eq>>(base : T) =>

class Ord extends base {

    lessOrEqual (another : this) : boolean {
        if (this.equal(another) || this.compare(another) === Ordering.LT) return true

        return false
    }

    compare (another : this) : Ordering {
        if (this.equal(another)) return Ordering.EQ

        if (this.lessOrEqual(another))
            return Ordering.LT
        else
            return Ordering.GT
    }
}

export type Ord = Mixin<typeof Ord>

Note, how inside the compare method we’ve used the equal method from the base mixin Eq. This typechecks correctly. If however, we would try to use some arbitrary call, it would raise a compilation error.

Now we can define a sort function, which works with any objects having the Ord mixin:

const sort = (array : Ord[]) : Ord[] => {

    return array.slice().sort((a, b) => {
        a.equal // typechecks
        a.zequal // compilation error

        switch (a.compare(b)) {
            case Ordering.LT: return -1;
            case Ordering.EQ: return 0;
            case Ordering.GT: return 1;
        }
    })
}

Again, notice how TypeScript is smart enough to figure out the type of the a and b arguments of the sorter function. This ensures that we can only use methods from Ord and Eq on them.

Now, we can define a Person class, instances of which are ordered by age:

class Person extends Ord(Eq(Object)) {
    age             : number

    constructor (age : number) {
        super(...arguments)
        
        this.age = age
    }

    equal (another : this) {
        return this.age === another.age
    }


    lessOrEqual (another : this) : boolean {
        return this.age <= another.age
    }
}

const person1   = new Person(11)
const person2   = new Person(22)

const sorted    = sort([ person2, person1 ])

Limitations of the mixin pattern

The mixin pattern is very close to the high-end code structuring abstractions available in other languages like Haskell or Rust. The differences are:

  1. Typeclasses can be implemented for the built-in types. In TypeScript, the best notation for a mixin working with a built-in type, like Array or Map is yet to be determined
  2. In Haskell, the namespaces of the typeclasses are different. This means typeclasses can use the same function name for completely non-related functionality. In TypeScript, you will get name conflicts. This issue happens very rarely in our experience however, and can always be fixed by choosing a different name for some property or method.

Drawbacks

The mixin pattern is currently at the cutting edge of TypeScript type checker possibilities. This unfortunately means that the support for mixins in the TypeScript is not first-class yet. Below you can read about the problems we experienced when using the mixin pattern extensively instead of classic inheritance. They are listed in the order of importance, starting with the most important ones. We encourage you to +1 the issues mentioned below (on GitHub) to help to draw the attention of the TypeScript team to improve the mixin support.

Compilation time

The compilation time for an application built with mixins can increase significantly even in a mid-size project. It feels like some algorithm in the compiler has quadratic behaviour based on the number of the mixins defined. So at some point, every new mixin added to your application slows down the compilation more and more.

The newly appeared “composite projects” feature of the TypeScript could come to the rescue, however, this issue blocks its usage for any non-trivial typed code.

Recursive Types problem

It is perfectly valid to define the recursively typed class definitions in TypeScript:

export class Class1 {
    another     : Class2
}

export class Class2 {
    another     : Class1
}

However, the same pattern for mixins does not compile:

export const SampleMixin1 = <T extends AnyConstructor<object>>(base : T) =>

class SampleMixin1 extends base {
    another             : SampleMixin2
}

export type SampleMixin1 = Mixin<typeof SampleMixin1>


export const SampleMixin2 = <T extends AnyConstructor<object>>(base : T) =>

class SampleMixin2 extends base {
    another             : SampleMixin1
}

export type SampleMixin2 = Mixin<typeof SampleMixin2>

This problem has been reported in this issue. The need for such typing appears pretty often. The workaround is to use an alternative notation for the mixin type instance with interfaces:

export const SampleMixin3 = <T extends AnyConstructor<object>>(base : T) =>

class SampleMixin3 extends base {
    another             : SampleMixin4
}

export interface SampleMixin3 extends Mixin<typeof SampleMixin3> {}


export const SampleMixin4 = <T extends AnyConstructor<object>>(base : T) =>

class SampleMixin4 extends base {
    another             : SampleMixin3
}

export interface SampleMixin4 extends Mixin<typeof SampleMixin4> {}

It is not clear however, if this notation affects the compilation time. Also this notation does not work cross-project. If you use it, you need to include all source files (even from another packages) into the include config in your tsconfig.json.

Generic argument problem

Currently it is not trivial to define a mixin with a generic argument. Let’s say we want to define a generic mixin wrapper for a value of arbitrary type V. The most natural definition would be:

export const Atom = <V, T extends AnyConstructor<object>>(base : T) =>

class Atom extends base {
    value       : V
}

export type Atom<V> = Mixin<typeof Atom<V>>

However, because of this issue, it does not compile. Basically the type application Atom<V> is only possible during a function call: Atom<V>() and can not be used in the context of value const AtomDate = Atom<Date>.

The workaround, (which works only on the class-level) is to use the extra dummy property VALUE_TYPE, typed as any in the base mixin. Everywhere where we need to reference the type of the value property we use this[ 'VALUE_TYPE' ] instead (this[ 'value' ] will work too actually).

export const Atom = <T extends AnyConstructor<object>>(base : T) =>

class Atom extends base {
    VALUE_TYPE  : any
    
    value       : this[ 'VALUE_TYPE' ]
}

// the "instance type" of this mixin
export type Atom = Mixin<typeof Atom>

Then, on the consuming side (a final class, or another mixin), we can upgrade the type of the VALUE_TYPE property:

class AtomDate extends Atom(Object) {
    VALUE_TYPE  : Date
}

Here, the VALUE_TYPE property plays the role of a generic type argument.

Decorators

When using decorators in a mixin, one can not use the compact arrow function notation, and is forced to use the full one.

export const SampleMixin1 = <T extends AnyConstructor<object>>(base : T) => {

    class SampleMixin1 extends base {
        @decorator
        property        : number
    }
    
    return SampleMixin1
}

export type SampleMixin1 = Mixin<typeof SampleMixin1>

This is a minor issue compared to the others, it simply causes one extra indentation level in the code. However, it would be great to see it solved.

Advanced usage

There’s nothing preventing us from supplying additional arguments for a mixin function, or for example, applying the same mixin function more than once. If we combine this with computed properties, we can implement various quite advanced scenarios. Of course, this should be used with care to not pollute your codebase with extra cognitive overhead, just because “it’s cool”. But a valid use case can be:

export const HasUniqueId = 
    <T extends AnyConstructor<object>>(base : T, idProperty: string | symbol = 'id') => 
{

    let ids : number        = 1

    class HasUniqueId extends base {
        // @ts-ignore
        [ idProperty ]  : number    = ids++
    }

    return HasUniqueId
}

export type HasUniqueId = Mixin<typeof HasUniqueId>

Unfortunately in the example above, TypeScript insists that TS1166: A computed property name in a class property declaration must refer to an expression whose type is a literal type or a 'unique symbol', so we have to convince it, that we know what we are doing with // @ts-ignore

To create a class implementing the Atom mixin from the previous example with a unique id in the non-standard ID property, we can do:

class AtomWithId extends HasUniqueId(Atom(Object), 'ID') {
    
}

Conclusion

In this post we demonstrated that the mixin pattern in TypeScript is comparable to more advanced code composition abstractions, found in Haskell (typeclasses), Rust (traits) and other languages. We provided a complete notation that scales well (type-safety wise) by the number of mixins, along with examples of its usage. The presented notation has been successfully used in our Bryntum products.

We also demonstrated the advantages of the mixin pattern over the classical single super class inheritance.

Lastly, we also highlighted some current drawbacks of using this approach, notably the increased compilation time and problems with recursive types and generic type arguments. We hope that the mixin pattern will become more widespread and that the support for it in TypeScript will become first-class. We encourage you to +1 the Github issues mentioned in this post to get attention from the TypeScript team.

In the meantime, if you find that some reusable logic in your application is tied too strictly to a certain super-class, you should definitely consider rewriting it as a mixin. And TypeScript will then ensure it is correctly used in every consuming class.

Happy mixing!

 

Nickolay Platonov

Development Tips 'n tricks