Mixins & Composition
JavaScript only supports single inheritance: a class can extends exactly one parent. That becomes a problem when unrelated classes need to share the same behavior — a Robot and a Bird both need to “fly,” but they have no sensible common ancestor. Mixins solve this by packaging reusable behavior as functions that augment a class, and composition lets you assemble objects from small, focused parts instead of forcing everything into a brittle hierarchy.
What is a mixin?
A mixin is a function that takes a base class and returns a new subclass with extra behavior layered on top. Because it is just a function, you can apply several mixins in a chain to compose exactly the capabilities a class needs.
const Serializable = (Base) => class extends Base {
toJSON() {
return JSON.stringify({ ...this });
}
};
const Timestamped = (Base) => class extends Base {
constructor(...args) {
super(...args);
this.createdAt = new Date();
}
};
class Model {}
class User extends Serializable(Timestamped(Model)) {
constructor(name) {
super();
this.name = name;
}
}
const u = new User("Ada");
console.log(u.createdAt instanceof Date);
console.log(u.toJSON());
Output:
true
{"createdAt":"2026-06-01T00:00:00.000Z","name":"Ada"}
Each mixin returns an anonymous class that extends whatever Base it is handed. Chaining Serializable(Timestamped(Model)) builds a prototype stack, so super(...args) correctly threads constructor arguments through every layer.
Always forward arguments with
super(...args)in a mixin constructor. Skipping it silently breaks initialization for any class lower in the chain.
Object-assign mixins
A lighter form copies methods directly onto a prototype with Object.assign. There is no subclass and no super chain — just a flat merge of methods. This is simple but cannot call overridden behavior on the host class.
const flyer = {
fly() {
return `${this.name} takes off`;
},
};
const swimmer = {
swim() {
return `${this.name} dives in`;
},
};
class Duck {
constructor(name) {
this.name = name;
}
}
Object.assign(Duck.prototype, flyer, swimmer);
const d = new Duck("Donald");
console.log(d.fly());
console.log(d.swim());
Output:
Donald takes off
Donald dives in
Function mixins vs object mixins
The two styles solve overlapping problems but differ in capability. Pick based on whether you need constructor logic or a super chain.
| Aspect | Function mixin (Base) => class | Object mixin Object.assign |
|---|---|---|
| Adds constructor logic | Yes, via super(...args) | No |
Supports super calls | Yes | No |
Works with instanceof | Yes (each layer is a class) | No (no new class created) |
| Syntax overhead | Higher | Minimal |
| Best for | Stateful, layered behavior | Stateless method bundles |
Composition over inheritance
Deep inheritance trees couple classes to ancestors they may not fully need, and changes ripple downward unpredictably. Composition flips the model: build behavior from small objects you delegate to, choosing capabilities per instance rather than per class.
const canFly = (state) => ({
fly: () => `flying at ${state.altitude}m`,
});
const canWalk = (state) => ({
walk: () => `walking on ${state.legs} legs`,
});
function createDrone(name) {
const state = { name, altitude: 120, legs: 0 };
return { name, ...canFly(state) };
}
function createDog(name) {
const state = { name, altitude: 0, legs: 4 };
return { name, ...canWalk(state) };
}
const drone = createDrone("Scout");
const dog = createDog("Rex");
console.log(drone.fly());
console.log(dog.walk());
Output:
flying at 120m
walking on 4 legs
Here each factory composes only the behaviors it needs. There is no class hierarchy to refactor when requirements change — you add or remove a capability function at the call site.
Reach for inheritance when you have a genuine “is-a” relationship and a stable taxonomy. Reach for composition when you have “can-do” capabilities that cut across unrelated types.
Detecting and naming mixins
Anonymous mixin classes make stack traces and name checks harder to read. Give the returned class a name and expose a brand for runtime detection.
const Disposable = (Base) =>
class Disposable extends Base {
static isDisposable = true;
dispose() {
this.disposed = true;
}
};
class Resource extends Disposable(Object) {}
console.log(Resource.isDisposable);
const r = new Resource();
r.dispose();
console.log(r.disposed);
Output:
true
true
Best practices
- Prefer composition for cross-cutting “can-do” behavior; reserve inheritance for true “is-a” relationships.
- Always call
super(...args)in function-mixin constructors so lower layers initialize correctly. - Keep each mixin focused on one responsibility — small mixins compose more cleanly than large ones.
- Name the class a mixin returns (
class Foo extends Base) so stack traces stay readable. - Avoid mixin name collisions; the last applied method wins silently and can mask bugs.
- Use
Object.assignmixins only for stateless method bundles that never needsuper.