Introducing Mixwith.ts and Mixins
- 07 Oct, 2024
Last summer I created a TypeScript library called mixwith.ts. This library is a Mixin library with TypeScript support that builds upon work from Justin Fagnani’s MixWith library for JavaScript.
What are mixins
A mixin is a way to add a set of reusable features to an existing entity such as a class. In the JavaScript ecosystem, Mixins fill a similar niche as base classes and object prototypes. Where mixins differ conceptually from classes and prototypes is that many mixins can be applied to a single TypeScript class.
Where mixins become useful is when you have common features that spans classes, but these classes do not naturally share an inheritance / prototype hierarchy.
An example
Suppose we are creating a TypeScript fantasy real time strategy game. In our initial prototype we will have a few different kinds of units. Let’s plan to have a series of mobile units: archers, knights, and siege engines. We’ll also have two fixed in place units, castles and towns. And maybe some flying units, dragons.
All unit need a position on the grid.
interface Position {
x: number;
y: number;
}
One way I could divide these units into an object hierarchy would be by their method of attack. Perhaps I would have:
class ShootingUnit implements Position {
x = 0; y = 0;
}
class MeleeUnit implements Position {
x = 0; y = 0;
}
class MagicUnit implements Position {
x = 0; y = 0;
}
But another way to share functionality between units may be by their mobility method.
class GroundUnit implements Position {
x = 0; y = 0;
}
class FlyingUnit implements Position {
x = 0;
y = 0;
}
class ImmobileUnit implements Position {
x = 0;
y = 0;
}
And in a real game, there may be multiple categories of overlapping features. Whichever hierarchy we choose, there is common functionality across units that isn’t captured cleanly.
If we try to combine these features into an object hierarchy, the combinatorics grow quickly and the inheritance hierarchy becomes less clean.
class MagicFlyingUnit {
}
//for some future magic tower attack
class MagicImmoileUnit {
}
Enter Mixins
For the purposes of this example, we decide that the mobility concept is where units share most of their implementation and go with these set of base classes:
class GroundUnit implements Position {
x = 0; y = 0;
}
class FlyingUnit implements Position {
x = 0; y = 0;
}
class ImmobileUnit implements Position {
x = 0;y = 0;
}
We can then construct a series of mixins that have the less “core” functionality of shooting, meleeing, and magic blasting (or whatever other shared functionality crosses our object hierarchy)
const shoot = <c extends Constructable>(s : c) => class extends s {
//shooting related properties and methods
shoot() { }
}
const melee = <c extends Constructable>(s : c) => class extends s {
//melee related properties and methods
melee() { }
}
const magic = <c extends Constructable>(s : c) => class extends s {
//magic related properties and methods
blast() { }
}
And now we can declare our unit classes using a combination of base classes with mixins and the library’s mix
and with
keywords:
//Declare an archer - A ground unit that shoots
class Archer extends mix(GroundUnit).with(shoot) {}
//Declare a castle - an immobile unit that can shoot
class Castle extends mix(ImmobileUnit).with(shoot) {}
//Declare a dragon - a magic unit that flies
class Dragon extends mix(FlyingUnit).with(magic) {}
Another benefit is that by wrapping these concepts up in mixins, when it comes time that we need to expand the feature set, we can.
Let’s say later on we wanted to add two new units. The first we want to add is a ninja. The ninja will have both shooting and melee. Easy enough:
class Ninja extends mix(GroundUnit).with(shoot, melee)
The second is a mounted dragon archer. We can take advantage of all the work we did in our Dragon
class, and append the shoot capability with a mixin:
class MountedDragonArcher extends mix(Dragon).with(shoot) {}
Why MixWith.ts
The original library was great, but I wanted the TypeScript goodness of intellisense and completions. And with this library, it works. You get not only intellisense, but type errors.
If you are interested in checking our the library, the source is available here