ai js mixins. 2026-09-12

By Xah Lee. Date: .

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:

How to choose

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.