When Steve Herell opened his first Ice Cream store, instead of having pre-mixed flavors, he had his staff mix freshly made ice cream with candy or other confections based on the customer requests.
Those candy additions were called mix-ins.
Steve probably only wanted to beat the competition, he didn't know that his mix-in would find its way into Computer Programming Languages years later, though not literally - we don't conjure ice creams with code yet. Do we, is there something you people are hiding from me?
The name found its way into Computer Science when a now-defunct computer manufacturing company, Symbolics Inc, introduced the term 'Mixin' in an early Object-Oriented Programming Language named Flavors and the name was inspired by Steve's Ice Cream parlor in Somerville, Massachusetts.
I bet we can all guess where programmers at Symbolics Inc spent their evenings.
Mixins in Programming Language
Just as Steve Herell's mix-ins were additions to the main thing, Mixins in Object-Oriented Programming Languages are also 'additions' to a class. Mixins allow a class to use 'functionality' defined in another class.
If you know about Class Inheritance, you might be convinced that we already do this. Let's take this example from the official TypeScript docs:
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log("Woof! Woof!");
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
Even though the Dog
class does not have a move
method but because it extends
the Animal
class, that is, it's inheriting the properties and methods of the Animal
class, we can safely write dog.move(10)
.
We say Dog
is the derived class while Animal
is the parent class.
Assuming we have another class, 'Mortal' and we want the Dog
class to also have its functionality (Dogs are mortals right?).
We might be tempted to do this:
class Dog extends Animal, Mortal {
...
}
TypeScript would scream if we try this, multiple inheritances are not supported by TypeScript.
This is why we need Mixins.
Mixins allow us to use functionality declared in a 'Base Class' in other classes without having to make the Base Class the parent of those classes. Rather than saying that we are inheriting from a Mixin, we usually say that the Mixin is "included" (i.e mixed in) into a class.
Mixins are not unique to TypeScript, they are a style of software development available in other programming languages too and the way they are "included" into classes (usually) differs across those languages.
How to use Mixins in TypeScript
Mixins in TypeScript are not actually the Classes we want to 'mix' but they are factories (functions) that take in a class and then 'mix' functionality from another class into it.
Mixins can be implemented in TypeScript as factories that create 'sub-classes'. Instead of class Dog
extending class Animal
, we write a function (the mixin), that takes in the Dog
class as an input and returns a new class with the Animal
functionality added (i.e 'mixed-in') to it. For a lack of better a better name, I call the class Animalled
:
The code snippet above implements the Animalled
mixin that we can use to add the 'Animal' functionality to any class, on line 15 we add it to the dog class.
If you looked carefully you would notice the type error on line 2, where I declared Base
as the function parameter. Since we are extending the Base
parameter, TypeScript complains that we should give the parameter a befitting type since if we don't specify the type, it defaults to any
.
So, we need to give Base
a type that indicates it can be used as a class, we need a constructor type.
type Constructor<T = {}> = new (...args: any[]) => T;
function Animalled<TBase extends Constructor>(Base: TBase) {
return class extends Base {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
}
class Dog {
bark() {
console.log("Woof! Woof!");
}
}
const AnimalledDog = Animalled(Dog);
let dog = new AnimalledDog();
dog.bark(); // "Woof! Woof!"
dog.move(10); // "Animal moved 10m."
The type Constructor<T>
is known as the construct signature. It describes the type as one that can be used to construct objects of the generic type T
, and T defaults to {}.
new (...args: any[])
tells us that the constructor function of this type accepts an arbitrary number of parameters of any type. Note that the name of the type could have been anything but Constructor
is the most suitable name.
We can now create another mixin for adding the Mortal
functionality:
type Constructor<T = {}> = new (...args: any[]) => T;
function Animalled<TBase extends Constructor>(Base: TBase) {
return class extends Base {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
}
function Mortalled<TBase extends Constructor>(Base: TBase) {
return class extends Base {
confess() {
console.log("I seek immortality!");
}
}
}
class Dog {
bark() {
console.log("Woof! Woof!");
}
}
const AnimalledDog = Animalled(Dog);
const MortalledAndAnimalledDog = Mortalled(AnimalledDog);
let dog = new MortalledAndAnimalledDog();
dog.bark(); // "Woof! Woof!"
dog.move(10); // "Animal moved 10m."
dog.confess(); // "I seek immortality!"
Fantabulous.
We can go on and on, giving the Dog
class more superpowers by mixing in more classes.
Summary
We have learnt that
[A mixin is] a function that
- takes a constructor
- declares a class that extends that constructor
- adds members to that new class
- and returns the class itself.
-- Announcing TypeScript 2.2 RC
There are so many ways you can use mixins, this post is already too long to touch some of them. However, I will provide links to some articles I find useful below, make sure to check them out.
Happy Coding.