Capítulo 23 de 36

Chapter 23: Mixins

Core Idea

Since TypeScript classes can't merge with other classes, mixins fill that gap: a generic function that takes a base class constructor and returns a new class expression extending it — TypeScript's compiler natively understands this pattern via code-flow analysis, without any special syntax.

Key Concepts

  • Constructor type as the generic constraint: type Constructor = new (...args: any[]) => {} types "any class constructor" — a mixin function is function Scale<TBase extends Constructor>(Base: TBase) { return class extends Base { ... } }, composed by calling it on a base class (Scale(Sprite)).
  • Mixins can't declare private/protected members in this pattern (only public and JS #private fields work) — a structural quirk of how the class-expression composition works.
  • Constrained mixins: a plain Constructor accepts any base class, which limits what the mixin body can assume. type GConstructor<T = {}> = new (...args: any[]) => T parameterizes the constructor type by its instance shape, letting a mixin require the base to already have specific members (e.g. GConstructor<{ setPos: (x: number, y: number) => void }>) — a mixin built on that constraint can safely call this.setPos(...), something an unconstrained Constructor wouldn't allow.
  • Alternative pattern — separate runtime + type merge: define each mixin as an ordinary class, apply them at runtime with a small applyMixins helper (copying prototype properties via Object.defineProperty), and separately declare interface Sprite extends Jumpable, Duckable {} merged with class Sprite {...} so the type system knows about members the runtime helper actually attached. This relies on the codebase keeping the runtime copy and the type-level interface extends in sync manually — less automatic than the class-expression pattern, but sometimes preferred for simpler cases.
  • Decorators can't drive mixins via type inference: a decorator that returns a class extending its target argument does add real members at runtime, but TypeScript's decorator typing does not merge those new members back onto the decorated class's type — accessing them requires a separate type-level workaround (e.g. an intersection type plus a type assertion), because decorator-based mixin composition isn't understood by the compiler's flow analysis the way the class-expression pattern is.
  • Static property mixin gotcha: the class-expression mixin pattern produces one concrete class per call, so its static members can't vary generically the way you might expect (class expressions effectively create per-application singletons at the type level). The workaround is wrapping the whole class declaration in its own generic function (function base<T>() { class Base { static prop: T } return Base }), so each instantiation of the function produces a distinct class with correctly-typed statics.

Code Examples

type GConstructor<T = {}> = new (...args: any[]) => T;
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class extends Base {
    jump() { this.setPos(0, 20); } // OK — constraint guarantees setPos exists
  };
}
  • What it demonstrates: a constrained mixin that can safely call a method the base class is guaranteed (by the Positionable constraint) to have, rather than accepting any arbitrary base.

Reference Tables

PatternType-runtime syncBest for
Class-expression mixin (function M<T extends Constructor>(Base: T))automaticmost cases — compiler-understood, composable
Separate classes + applyMixins + interface extends mergemanualsimpler cases, or when class-expression composition is awkward
Generic-function-wrapped class (for static members)automaticmixins that need per-instantiation static typing

Key Takeaways

  1. Prefer the class-expression mixin pattern (<TBase extends Constructor>) as the default — it's the one TypeScript's compiler natively understands via flow analysis.
  2. Constrain the base constructor type (GConstructor<RequiredShape>) whenever a mixin needs to call methods the base is expected to already provide — an unconstrained Constructor accepts any base, including ones missing what the mixin needs.
  3. Don't rely on decorators to compose mixins — the added members won't show up in the decorated class's inferred type without a manual type-level workaround.

Connects To

  • Generics: mixins are fundamentally generic functions over constructor types.
  • Classes — Advanced: the constructor-signature pattern (new () => T) used here is the same one covered for abstract-class factories.
  • Declaration Merging: the alternative pattern's interface extends step relies directly on interface merging rules.