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 mixin pattern in TypeScript that we use heavily at Bryntum for development of our products. It allows us to write the very clean code and is very close to the “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.

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 the 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:

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:

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:

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:

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:

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:

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:

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:

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:

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

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:

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:

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).

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:

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!

Share:

Comments ( 4 )

Igor 2019-03-12

Great article. There is no much material on TypeScript mixins. Is there github repo with examples/working project using them?

Thank you. Yes, there’s a “chronograph” – graph based computational engine, that powers the upcoming Bryntum Gantt, it is written using mixins instead of classic single class inheritance: https://github.com/bryntum/chronograph/