Skip to content
JavaScript js classes 4 min read

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.

AspectFunction mixin (Base) => classObject mixin Object.assign
Adds constructor logicYes, via super(...args)No
Supports super callsYesNo
Works with instanceofYes (each layer is a class)No (no new class created)
Syntax overheadHigherMinimal
Best forStateful, layered behaviorStateless 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.assign mixins only for stateless method bundles that never need super.
Last updated June 1, 2026
Was this helpful?