ai js mixins. 2026-09-12
Explore JavaScript Mixin Patterns
(ai answer)
A mixin is a reusable bundle of behavior you fold into objects or classes that are not related by a single inheritance line. JavaScript only has single inheritance, so mixins are how people share orthogonal concerns—logging, dirty-tracking, serialization, events—without forcing everything into one hierarchy.
There are four patterns that actually show up in real code. They are not interchangeable.
1. Concatenative mixins (Object.assign)
Copy properties from one or more source objects onto a target. Eric Elliott calls this concatenative inheritance.
Onto an instance:
const canFly = { fly() { return `${this.name} flies`; } }; const canSwim = { swim() { return `${this.name} swims`; } }; function createDuck(name) { return Object.assign({ name }, canFly, canSwim); } createDuck("Mallard").fly(); // "Mallard flies"
Onto a class prototype (one copy, shared by all instances):
const dirtyTrackable = { markDirty() { this._dirty = true; }, markClean() { this._dirty = false; }, isDirty() { return !!this._dirty; }, }; class Document { constructor(title) { this.title = title; } } Object.assign(Document.prototype, dirtyTrackable);
Later sources win on name clashes. Object.assign only copies enumerable own properties, so it will not copy class methods (they are non-enumerable). Copy descriptors if you need getters:
function applyTraits(Ctor, ...traits) { for (const trait of traits) { for (const key of Reflect.ownKeys(trait)) { if (key === "constructor") continue; Object.defineProperty( Ctor.prototype, key, Object.getOwnPropertyDescriptor(trait, key) ); } } return Ctor; }
Strengths: tiny, works on plain objects, no extra classes.
Weaknesses: no super, constructors in the mixin are ignored, name collisions are silent, private fields (#x) cannot live on a copied object in a useful way.
2. Subclass-factory mixins (the modern class pattern)
A mixin is a function: Base => class extends Base { ... }. Each application is a real subclass, so super and constructors work.
const Timestamped = (Base) => class extends Base { constructor(...args) { super(...args); this.createdAt = new Date(); } touch() { this.updatedAt = new Date(); } }; const Versioned = (Base) => class extends Base { constructor(...args) { super(...args); this.version = 1; } bump() { this.version += 1; } }; class Note { constructor(text) { this.text = text; } } class TrackedNote extends Versioned(Timestamped(Note)) {} const n = new TrackedNote("hello"); n.touch(); n.bump();
Apply order is right-to-left in the nesting: Versioned(Timestamped(Note)) means Note is the innermost superclass, then Timestamped, then Versioned, then TrackedNote.
Libraries such as mixwith.js add caching and deduping so the same mixin is not applied twice on a long chain.
Mixin methods should extend the base, not silently replace it:
const Logged = (Base) => class extends Base { save(...args) { const result = super.save?.(...args); console.log("saved", this); return result; } };
Strengths: super works, constructors compose, TypeScript has a documented mixin-class pattern, instanceof along the chain is honest.
Weaknesses: each application creates a new class (startup cost if you do it wildly), diamond-shaped reuse still needs deduping, private fields in a mixin stay private to that class layer.
3. Functional mixins / factory composition
A function takes state and returns a bag of methods. You compose factories instead of classes.
const withFlying = (o) => ({ ...o, fly() { return `${o.name} flies`; }, }); const withSwimming = (o) => ({ ...o, swim() { return `${o.name} swims`; }, }); const duck = withSwimming(withFlying({ name: "Duck" }));
Or close over private state (true encapsulation, no prototype sharing):
function withCounter(o = {}) { let n = 0; return { ...o, inc() { return ++n; }, value() { return n; }, }; }
This is what people mean by “functional mixins.” Methods are often per-instance (memory) unless you still hang them on a shared prototype.
Strengths: easy to reason about, great with plain data, no this surprises if you avoid it.
Weaknesses: easy to copy methods onto every object; less natural for instanceof and class-oriented APIs.
4. Delegation / “has-a” composition (often better than a mixin)
Instead of mixing methods onto this, hold a collaborator:
class DirtyTracker { #dirty = false; markDirty() { this.#dirty = true; } markClean() { this.#dirty = false; } isDirty() { return this.#dirty; } } class Document { constructor(title) { this.title = title; this.dirty = new DirtyTracker(); } } const d = new Document("Notes"); d.dirty.markDirty();
No name collisions, private state stays private, you can swap the collaborator in tests. This is the default recommendation when the behavior is not truly “this object is also X.”
Conflicts and other sharp edges
| Issue | Concatenative | Subclass factory | Has-a composition |
|---|---|---|---|
| Name clash | last assign wins | last mixin in the chain wins; subclass can override | names live on the collaborator |
super |
no | yes | call the collaborator explicitly |
| Constructor / init | you must call an init() yourself |
constructor + super(...args) |
construct the part in your constructor |
Private #fields |
do not mix well | each layer has its own privates | natural |
instanceof mixin |
false | true for each generated class | false (by design) |
| Performance | mutating a hot prototype can deopt; copying per instance costs memory | extra classes at load | usually cheapest mentally and at runtime |
Also:
- Do not use mixins to share identity (“a Duck is an Animal”). That is still inheritance.
- Mixins that both own the same piece of state (
this._dirty) will stomp each other. - Changing
[[Prototype]]withObject.setPrototypeOfto “mix in” a parent is the wrong tool: slow and surprising. Mixins copy or wrap; they should not reparent live objects in a hot path.
How to choose
- Orthogonal methods, no
super, plain objects or a single prototype:Object.assign/ trait object. - Layered behavior that must call through the chain, constructors, TypeScript classes: subclass factory.
- State that should stay encapsulated, or behavior you might swap: a collaborator object (composition).
- UI frameworks: mixins are largely legacy (React dropped them; Vue 3 prefers composition APIs). Prefer hooks / composables / collaborator objects.
Rule of thumb from current practice: use a mixin when the behavior is genuinely part of the object’s public surface and would be awkward as a separate object. Otherwise compose.