TypeScript Mixin
Un Mixin es un patrón de reutilización de código que se utiliza para incorporar múltiples módulos de funcionalidad independientes en una clase.
TypeScript implementa Mixins mediante funciones genéricas que devuelven clases extendidas, lo que compensa la limitación de la herencia única de no poder reutilizar comportamientos de múltiples fuentes.
Diagrama SVG: proceso de composición de MixinConceptos básicos
El núcleo de un Mixin es una función genérica que recibe una clase base y devuelve una clase extendida.ConstructorLa restricción de tipo se utiliza para describir "cualquier clase que pueda ser heredada".
Ejemplo
type Constructor<T = {}> = new (...args: any[]) => T;
// Función Mixin: recibe la clase base y devuelve la nueva clase extendida
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
// Clase base
class User {
constructor(public name: string) {}
}
// Tras la mezcla se obtiene una nueva clase
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");
console.log(user.name); // Alice
console.log(user.createdAt instanceof Date); // true
Resultado de ejecución:
Alice true
Nota:Mixin no modifica la clase original, sino que devuelve una nueva clase extendida; la clase
Useroriginal no se ve afectada.
Combinar múltiples Mixins
Al invocar varias funciones Mixin de forma anidada, se pueden superponer múltiples capacidades en la misma clase.
Ejemplo
// Mixin 1: agrega marca de tiempo
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
// Mixin 2: agrega serialización
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this);
}
};
}
// Mixin 3: agrega registro (depende del método serialize)
function Loggable<TBase extends Constructor<{ serialize(): string }>>(Base: TBase) {
return class extends Base {
log(): void {
console.log("[LOG]", this.serialize());
}
};
}
class Product {
constructor(public name: string, public price: number) {}
}
// Superpone los tres Mixins en orden
const AdvancedProduct = Loggable(Serializable(Timestamped(Product)));
const p = new AdvancedProduct("Phone", 999);
p.log();
console.log(p.createdAt instanceof Date); // true
Resultado de ejecución:
[LOG] {"name":"Phone","price":999,"createdAt":"2026-..."}
true
Combinación de Mixin con interfaces
La clase devuelta por un Mixin puede declarar que implementa una interfaz, permitiendo al consumidor operar con el objeto mezclado a través del tipo de interfaz, sin depender de la implementación concreta.
Ejemplo
// Define la interfaz de capacidad
interface ISerializable {
serialize(): string;
}
interface ICloneable<T> {
clone(): T;
}
// Mixin implementa la interfaz
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base implements ISerializable {
serialize(): string {
return JSON.stringify(this);
}
};
}
function Cloneable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
clone() {
return Object.assign(
Object.create(Object.getPrototypeOf(this)),
this
);
}
};
}
class Article {
constructor(public title: string, public content: string) {}
}
const RichArticle = Cloneable(Serializable(Article));
const a1 = new RichArticle("Introducción a TypeScript", "Contenido del cuerpo...");
const a2 = a1.clone();
a2.title = "TypeScript avanzado";
console.log(a1.serialize());
// {"title":"Introducción a TypeScript","content":"Contenido del cuerpo..."}
console.log(a2.serialize());
// {"title":"TypeScript avanzado","content":"Contenido del cuerpo..."}
console.log(a1.title === a2.title); // false (modificación independiente tras la clonación)
Resultado de ejecución:
{"title":"TypeScript 入门","content":"正文内容..."}
{"title":"TypeScript 进阶","content":"正文内容..."}
false
Mixin con restricciones
Mediante restricciones genéricas, se puede limitar el Mixin para que solo se aplique a clases que cumplan condiciones específicas, evitando la falta de propiedades necesarias en tiempo de ejecución.
Ejemplo
// Restricción: la clase base debe tener las propiedades id y name
type WithIdAndName = Constructor<{ id: number; name: string }>;
function Printable<TBase extends WithIdAndName>(Base: TBase) {
return class extends Base {
print(): void {
console.log(`[${this.id}] ${this.name}`);
}
};
}
class Item {
constructor(public id: number, public name: string) {}
}
// Correcto: Item cumple la restricción
const PrintableItem = Printable(Item);
const item = new PrintableItem(42, "Keyboard");
item.print(); // [42] Keyboard
// Ejemplo incorrecto (el compilador lo impedirá):
// class NoId { constructor(public name: string) {} }
// const Bad = Printable(NoId); // Error: a NoId le falta la propiedad id
Resultado de ejecución:
[42] Keyboard
Comparación entre Mixin y herencia
| Dimensión | Herencia (extends) | Mixin |
|---|---|---|
| Cantidad de fuentes | Solo puede heredar de una clase padre | Se pueden superponer cualquier cantidad |
| Grado de acoplamiento | Fuerte acoplamiento entre subclase y clase padre | Cada Mixin es independiente, bajo acoplamiento |
| Granularidad de reutilización | Reutiliza la capacidad de toda la clase | Reutiliza una única función según necesidad |
| Seguridad de tipos | Soporte nativo | Debe garantizarse mediante restricciones genéricas |
| Casos de uso | Relación "is-a" fuerte | Aspectos transversales (registro, serialización, caché, etc.) |
Resumen
- Patrón central:Mixin es una función genérica que recibe una clase base y devuelve una clase extendida,
Constructor<T>es el tipo de restricción estándar - Superposición de capacidades:Al invocar de forma anidada varias funciones Mixin, se pueden combinar múltiples capacidades en la misma clase
- Combinación con interfaces:La clase devuelta por Mixin puede implementar interfaces; el consumidor solo necesita depender de la interfaz en lugar de la clase concreta
- Restricciones genéricas:Mediante restricciones
TBasese puede limitar el ámbito de aplicación del Mixin y evitar usos incorrectos en tiempo de compilación - Casos de uso:Aspectos transversales como registro, serialización, clonación, marcas de tiempo, etc., superiores a la herencia multicapa