混入

除了传统的 OO 层次结构之外,另一种从可重用组件构建类的流行方法是通过组合更简单的部分类来构建它们。你可能熟悉 Scala 等语言的 mixin 或特性的概念,并且该模式在 JavaScript 社区中也颇为流行。

¥Along with traditional OO hierarchies, another popular way of building up classes from reusable components is to build them by combining simpler partial classes. You may be familiar with the idea of mixins or traits for languages like Scala, and the pattern has also reached some popularity in the JavaScript community.

混入是如何工作的?

¥How Does A Mixin Work?

该模式依赖于使用具有类继承的泛型来扩展基类。TypeScript 最好的 mixin 支持是通过类表达式模式完成的。你可以阅读有关此模式在 JavaScript 此处 中如何工作的更多信息。

¥The pattern relies on using generics with class inheritance to extend a base class. TypeScript’s best mixin support is done via the class expression pattern. You can read more about how this pattern works in JavaScript here.

开始之前,我们需要一个类,它会将 mixins 应用在:

¥To get started, we’ll need a class which will have the mixins applied on top of:

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

然后你需要一个类型和一个工厂函数,它返回一个扩展基类的类表达式。

¥Then you need a type and a factory function which returns a class expression extending the base class.

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 的基类:

¥With these all set up, then you can create a class which represents the base class with mixins applied:

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

受约束的混入

¥Constrained Mixins

在上面的表格中,mixin 没有类的基础知识,这使得很难创建你想要的设计。

¥In the above form, the mixin’s have no underlying knowledge of the class which can make it hard to create the design you want.

为了对此建模,我们修改原始构造函数类型以接受泛型参数。

¥To model this, we modify the original constructor type to accept a generic argument.

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

这允许创建仅适用于受约束基类的类:

¥This allows for creating classes which only work with constrained base classes:

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

然后,你可以创建仅在你有特定基础可以构建时才起作用的 mixin:

¥Then you can create mixins which only work when you have a particular base to build on:

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

替代模式

¥Alternative Pattern

本文档的早期版本推荐了一种编写 mixin 的方法,你可以分别创建运行时和类型层次结构,然后在最后合并它们:

¥Previous versions of this document recommended a way to write mixins where you created both the runtime and type hierarchies separately, then merged them at the end:

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

这种模式较少依赖编译器,而更多地依赖于你的代码库,以确保运行时和类型系统正确保持同步。

¥This pattern relies less on the compiler, and more on your codebase to ensure both runtime and type-system are correctly kept in sync.

约束条件

¥Constraints

通过代码流分析,TypeScript 编译器原生支持 mixin 模式。在某些情况下,你可以触及原生支持的边缘。

¥The mixin pattern is supported natively inside the TypeScript compiler by code flow analysis. There are a few cases where you can hit the edges of the native support.

装饰器和混入 #4881

¥Decorators and Mixins #4881

你不能使用装饰器通过代码流分析来提供混入:

¥You cannot use decorators to provide mixins via code flow analysis:

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

静态属性混入 #17829

¥Static Property Mixins #17829

更多的是一个陷阱而不是一个约束。类表达式模式创建单例,因此它们不能在类型系统上映射以支持不同的变量类型。

¥More of a gotcha than a constraint. The class expression pattern creates singletons, so they can’t be mapped at the type system to support different variable types.

你可以通过使用函数返回基于泛型不同的类来解决此问题:

¥You can work around this by using functions to return your classes which differ based on a generic:

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