除了传统的 OO(Object-Oriented) 范式之外,我们也可以通过“组件复用” 的方式构建类,即将一些小的类组合起来,构建一个大的类。如果你对 mixins 和 traits 比较熟悉的话,这类模式在 JavaScript 的世界里也是非常流行的。
Mixin 如何工作呢?(How Does A Mixin Work?)
Mixin 的基本思路是结合使用泛型和类的继承来扩展一个基础类型。TypeScript 对 Mixin 的支持主要通过类表达式模式(class expression pattern)体现,你可以通过 这里 了解这个模式在 JavaScript 中的作用机制。
开始之前,我们需要创建一个基础类,mixins 将会应用到这个类上。
class Sprite {name = "";x = 0;y = 0;constructor(name: string) {this.name = name;}}
然后我们要创建一个类型和一个工厂函数,工厂函数返回一个扩展了基础类的类表达式。
// 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;}};}
至此,我们就可以创建一个应用了 Mixins 的基础类了。
// 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);
Mixins 约束(Constrained Mixins)
在上面的例子中,Mixins 不知道目标类的任何信息,功能非常局限。我们可以调整一下构建函数的类型:
// 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 totype GConstructor<T = {}> = new (...args: any[]) => T;
这样我们就可以约束 Mixins 的作用目标了:
// 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 totype GConstructor<T = {}> = new (...args: any[]) => T;
可以创建只作用于目标基类的 Mixins。
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);}};}
另外一种方式(Alternative Pattern)
这篇文档的之前版本中推荐了另外一种写 Mixin 的方法:先分别创建运行时和类型规则,然后将它们组合在一起,这样表达不太直观, 直接看代码:
// Each mixin is a traditional ES classclass Jumpable {jump() {}}class Duckable {duck() {}}// Including the baseclass Sprite {x = 0;y = 0;}// Then you create an interface which merges// the expected mixins with the same name as your baseinterface Sprite extends Jumpable, Duckable {}// Apply the mixins into the base class via// the JS at runtimeapplyMixins(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));});});}
这种方式对编译器的依赖较低,更多地需要代码的运行时部分和类型部分保持准确的匹配。
限制(Constraints)
Mixin 在 TypeScript 内部是通过代码流分析(code flow analysis)实现的,您可能遇到一些边界情况。
装饰器和 Mixin(Decorators and Mixins)
不可以通过装饰器来应用 Mixin。
// A decorator function which replicates the mixin pattern:const Pausable = (target: typeof Player) => {return class Pausable extends target {shouldFreeze = false;};};@Pausableclass Player {x = 0;y = 0;}// The Player class does not have the decorator's type merged:const player = new Player();player.shouldFreeze;// Error: Property 'shouldFreeze' does not exist on type 'Player'.// It 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;
静态属性 Mixin(Static Property Mixins)
这一点更像是一个特性而不是限制。类表达式模式创建的是一个单例(singleton),所以它们并不能映射到类型系统中以支持不同的可变类型。
换一种写法就好了:
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; // stringSpec.anotherProp; // string
