混入 (Mixins)

除了傳統的面向物件層級結構之外,透過組合簡單的部分類(partial classes)來構建可重用的元件也是一種流行的方法。你可能熟悉 Scala 等語言中的 Mixins(混入)或 Traits(特徵)概念,這種模式在 JavaScript 社群中也頗受歡迎。

Mixins 是如何工作的?

這種模式依賴於將泛型與類繼承結合使用來擴充套件基類。TypeScript 對 Mixins 最好的支援是透過類表示式(class expression)模式實現的。你可以在這裡閱讀更多關於此模式在 JavaScript 中如何工作的內容。

首先,我們需要一個將被應用 Mixins 的基礎類。

ts
class Sprite {
name = "";
x = 0;
y = 0;
 
constructor(name: string) {
this.name = name;
}
}
Try

然後,你需要一個型別和一個工廠函式,該函式返回一個擴充套件了基類的類表示式。

ts
// To get started, we need a type which we'll use to extend
// other classes from. The main responsibility is to declare
// that the type being passed in is a class.
 
type Constructor = new (...args: any[]) => {};
 
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
 
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;
 
setScale(scale: number) {
this._scale = scale;
}
 
get scale(): number {
return this._scale;
}
};
}
Try

準備好這些之後,你就可以建立一個類,它代表了應用了 Mixins 的基類。

ts
// Compose a new class from the Sprite class,
// with the Mixin Scale applier:
const EightBitSprite = Scale(Sprite);
 
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);
Try

受約束的 Mixins (Constrained Mixins)

在上面的形式中,Mixin 對底層類沒有任何預知,這可能使建立你想要的設計變得困難。

為了對此進行建模,我們修改原始建構函式型別,使其接受一個泛型引數。

ts
// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor<T = {}> = new (...args: any[]) => T;
Try

這允許建立僅適用於受約束基類的類。

ts
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
Try

然後,你可以建立僅在擁有特定構建基類時才生效的 Mixins。

ts
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// This mixin will only work if it is passed a base
// class which has setPos defined because of the
// Positionable constraint.
this.setPos(0, 20);
}
};
}
Try

替代模式

該文件的早期版本推薦了一種編寫 Mixins 的方式:分別建立執行時和型別層級,最後再將它們合併。

ts
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}
 
class Duckable {
duck() {}
}
 
// Including the base
class Sprite {
x = 0;
y = 0;
}
 
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
 
let player = new Sprite();
player.jump();
console.log(player.x, player.y);
 
// This can live anywhere in your codebase:
function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
});
});
}
Try

這種模式對編譯器的依賴較少,而更多地依賴於你的程式碼庫來確保執行時和型別系統保持同步。

限制

Mixin 模式透過程式碼流分析在 TypeScript 編譯器中得到原生支援。在極少數情況下,你可能會遇到原生支援的邊緣情況。

裝飾器與 Mixins #4881

你不能使用裝飾器透過程式碼流分析來提供 Mixins。

ts
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};
 
@Pausable
class Player {
x = 0;
y = 0;
}
 
// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;
Property 'shouldFreeze' does not exist on type 'Player'.2339Property 'shouldFreeze' does not exist on type 'Player'.
 
// The runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = Player & { shouldFreeze: boolean };
 
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;
Try

靜態屬性 Mixins #17829

這與其說是一個限制,不如說是一個“陷阱”。類表示式模式建立的是單例,因此它們無法在型別系統中進行對映以支援不同的變數型別。

你可以透過使用函式來返回基於泛型而不同的類來繞過這個問題。

ts
function base<T>() {
class Base {
static prop: T;
}
return Base;
}
 
function derived<T>() {
class Derived extends base<T>() {
static anotherProp: T;
}
return Derived;
}
 
class Spec extends derived<string>() {}
 
Spec.prop; // string
Spec.anotherProp; // string
Try

TypeScript 文件是一個開源專案。歡迎提交 Pull Request 來幫助我們改進這些頁面 ❤

此頁面的貢獻者
OTOrta Therox (16)
GMGleb Maksimenko (1)
IOIván Ovejero (1)
DEDom Eccleston (1)
OOblosys (1)
5+

最後更新:2026 年 3 月 27 日