A mixin is a pattern that lets you combine behaviour from multiple sources into a single class. TypeScript does not support multiple inheritance — a class cannot extend more than one base class — but mixins solve this elegantly.
Mixin là một pattern cho phép bạn kết hợp hành vi từ nhiều nguồn khác nhau vào một class duy nhất. TypeScript không hỗ trợ đa kế thừa (một class không thể extends nhiều class cùng lúc), nhưng mixins giải quyết vấn đề này một cách thanh lịch.
Instead of direct inheritance, a mixin is a function that takes a class as input and returns a new, extended class. You can stack multiple mixins to compose a feature-rich class from small, focused pieces.
Thay vì kế thừa trực tiếp, mixin hoạt động như một hàm nhận một class làm đầu vào và trả về một class mới đã được mở rộng. Bạn có thể 'xếp chồng' nhiều mixin lên nhau để tạo ra một class giàu tính năng.
To write type-safe mixins in TypeScript you need a type that represents "any class that can be instantiated". This is the foundation every mixin is built on.
Để viết mixin an toàn về kiểu trong TypeScript, bạn cần một kiểu đại diện cho 'bất kỳ class nào có thể được khởi tạo'. Đây là nền tảng của mọi mixin.
// A generic constructor type
// GInstance defaults to {} so any instance shape is accepted
type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;GConstructor<T> is a constructor type that accepts any number of arguments and produces an instance of type T. The GInstance type parameter lets you constrain the input class to have a particular shape.
GConstructor<T> là một kiểu hàm tạo nhận bất kỳ số lượng đối số nào và tạo ra một instance của kiểu T. Tham số kiểu GInstance cho phép bạn ràng buộc class đầu vào phải có một 'hình dạng' nhất định.
Here is a first mixin: Timestamped. It accepts any class and adds a createdAt property to it.
Dưới đây là mixin đầu tiên: Timestamped. Nó nhận một class bất kỳ và thêm thuộc tính createdAt vào đó.
1type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;23function Timestamped<TBase extends GConstructor>(Base: TBase) {4 return class extends Base {5 createdAt = new Date();6 };7}89// Apply the mixin10class User {11 constructor(public name: string) {}12}1314const TimestampedUser = Timestamped(User);15const user = new TimestampedUser("Alice");1617console.log(user.name); // "Alice"18console.log(user.createdAt); // current DateThe Timestamped function takes a class (Base) and returns an anonymous class that extends it with the new property. The return type is inferred by TypeScript, so user has full type information for both name and createdAt.
Hàm Timestamped nhận một class (Base) và trả về một class ẩn danh extends Base với thuộc tính mới. Kiểu trả về được TypeScript tự động suy ra, vì vậy user có đầy đủ thông tin kiểu cho cả name và createdAt.
The real power of mixins appears when you compose them. Each mixin only knows about its own base class and adds one focused capability.
Sức mạnh thực sự của mixins xuất hiện khi bạn kết hợp chúng lại. Mỗi mixin chỉ biết về Base class của nó và thêm một tính năng duy nhất.
1type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;23// Mixin 1: adds createdAt4function Timestamped<TBase extends GConstructor>(Base: TBase) {5 return class extends Base {6 createdAt = new Date();7 };8}910// Mixin 2: adds activate / deactivate11function Activatable<TBase extends GConstructor>(Base: TBase) {12 return class extends Base {Thứ tự áp dụng mixin có thể ảnh hưởng đến hành vi nếu nhiều mixin định nghĩa cùng một thuộc tính hoặc phương thức. Mixin được áp dụng cuối cùng (ngoài cùng) sẽ ghi đè các mixin bên trong.
Sometimes a mixin only makes sense applied to classes with a particular shape. You can use a generic constraint to enforce this at compile time.
Đôi khi một mixin chỉ có ý nghĩa khi được áp dụng cho các class có một hình dạng nhất định. Bạn có thể dùng ràng buộc generic để thực thi điều này tại thời điểm biên dịch.
1type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;23// This mixin requires the base class to have a name property4type Named = GConstructor<{ name: string }>;56function Greeter<TBase extends Named>(Base: TBase) {7 return class extends Base {8 greet() {9 return `Hello, I'm ${this.name}`;10 }11 };12}An idiomatic way to declare the type of a composed class is to merge an interface with the class. This lets you describe the resulting shape without relying solely on the inferred type.
Một cách thành ngữ để khai báo kiểu của class kết hợp là merge interface với class. Điều này cho phép bạn mô tả kiểu kết quả mà không cần dựa vào kiểu được suy ra.
1type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;23function Timestamped<TBase extends GConstructor>(Base: TBase) {4 return class extends Base {5 createdAt = new Date();6 };7}89function Activatable<TBase extends GConstructor>(Base: TBase) {10 return class extends Base {11 isActive = false;12 activate() { this.isActive = true; }Here is a more realistic example: a mixin that adds basic CRUD tracking to any entity class.
Hãy xem một ví dụ thực tế hơn: một mixin thêm khả năng CRUD cơ bản vào bất kỳ entity class nào.
1type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;23function Auditable<TBase extends GConstructor>(Base: TBase) {4 return class extends Base {5 createdAt: Date = new Date();6 updatedAt: Date = new Date();7 createdBy = "system";89 touch(by: string) {10 this.updatedAt = new Date();11 this.createdBy = by;12 }Mixins cannot access private members of the base class because they are declared outside it. Use protected if you need access from a mixin.
Mixins không thể truy cập các private member của class đầu vào vì chúng được khai báo bên ngoài class đó. Hãy dùng protected nếu bạn cần khả năng truy cập từ mixin.
Because mixins produce anonymous classes, instanceof may not work as expected against intermediate classes. Check against the final composed class, not individual mixins.
Vì mixin tạo ra các class ẩn danh, instanceof có thể không hoạt động như mong đợi với các class trung gian. Hãy kiểm tra instance của class kết quả cuối cùng, không phải của mixin riêng lẻ.
If a mixin needs its own constructor parameters you must handle argument forwarding manually. This is why the ...args: unknown[] signature is used — it is compatible with any base constructor.
Nếu mixin cần nhận tham số constructor riêng, bạn phải xử lý việc chuyển tiếp tham số một cách thủ công. Đây là lý do tại sao chữ ký ...args: unknown[] được dùng — nó tương thích với bất kỳ constructor nào.
1type GConstructor<GInstance = {}> = new (...args: unknown[]) => GInstance;23function Tagged<TBase extends GConstructor>(Base: TBase) {4 return class extends Base {5 tag: string;67 constructor(...args: unknown[]) {8 super(...args); // forward all args to the base class9 this.tag = "default";10 }1112 withTag(tag: string): this {Mixin là hàm nhận một class và trả về class mới đã được mở rộng
GConstructor<T> là kiểu nền tảng giúp mixin an toàn về kiểu
C(B(A(Base)))Bạn có thể xếp chồng nhiều mixin: C(B(A(Base)))
Dùng ràng buộc generic để yêu cầu class đầu vào có hình dạng nhất định
Mixins không thể truy cập private members — hãy dùng protected
instanceof hoạt động với class kết quả cuối cùng, không phải với mixin trung gian