Capítulo 23 de 36
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.
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)).private/protected members in this pattern (only public and JS #private fields work) — a structural quirk of how the class-expression composition works.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.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.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.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.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
};
}
Positionable constraint) to have, rather than accepting any arbitrary base.| Pattern | Type-runtime sync | Best for |
|---|---|---|
Class-expression mixin (function M<T extends Constructor>(Base: T)) | automatic | most cases — compiler-understood, composable |
Separate classes + applyMixins + interface extends merge | manual | simpler cases, or when class-expression composition is awkward |
| Generic-function-wrapped class (for static members) | automatic | mixins that need per-instantiation static typing |
<TBase extends Constructor>) as the default — it's the one TypeScript's compiler natively understands via flow analysis.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.new () => T) used here is the same one covered for abstract-class factories.interface extends step relies directly on interface merging rules.