原文地址:Mixins - Reference | TypeScript Docs

除了传统的 OO(Object-Oriented) 范式之外,我们也可以通过“组件复用” 的方式构建类,即将一些小的类组合起来,构建一个大的类。如果你对 mixins 和 traits 比较熟悉的话,这类模式在 JavaScript 的世界里也是非常流行的。

Mixin 如何工作呢?(How Does A Mixin Work?)

Mixin 的基本思路是结合使用泛型和类的继承来扩展一个基础类型。TypeScript 对 Mixin 的支持主要通过类表达式模式(class expression pattern)体现,你可以通过 这里 了解这个模式在 JavaScript 中的作用机制。

开始之前,我们需要创建一个基础类,mixins 将会应用到这个类上。

  1. class Sprite {
  2. name = "";
  3. x = 0;
  4. y = 0;
  5. constructor(name: string) {
  6. this.name = name;
  7. }
  8. }

然后我们要创建一个类型和一个工厂函数,工厂函数返回一个扩展了基础类的类表达式。

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

至此,我们就可以创建一个应用了 Mixins 的基础类了。

  1. // Compose a new class from the Sprite class,
  2. // with the Mixin Scale applier:
  3. const EightBitSprite = Scale(Sprite);
  4. const flappySprite = new EightBitSprite("Bird");
  5. flappySprite.setScale(0.8);
  6. console.log(flappySprite.scale);

Mixins 约束(Constrained Mixins)

在上面的例子中,Mixins 不知道目标类的任何信息,功能非常局限。我们可以调整一下构建函数的类型:

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

这样我们就可以约束 Mixins 的作用目标了:

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

可以创建只作用于目标基类的 Mixins。

  1. function Jumpable<TBase extends Positionable>(Base: TBase) {
  2. return class Jumpable extends Base {
  3. jump() {
  4. // This mixin will only work if it is passed a base
  5. // class which has setPos defined because of the
  6. // Positionable constraint.
  7. this.setPos(0, 20);
  8. }
  9. };
  10. }

另外一种方式(Alternative Pattern)

这篇文档的之前版本中推荐了另外一种写 Mixin 的方法:先分别创建运行时和类型规则,然后将它们组合在一起,这样表达不太直观, 直接看代码:

  1. // Each mixin is a traditional ES class
  2. class Jumpable {
  3. jump() {}
  4. }
  5. class Duckable {
  6. duck() {}
  7. }
  8. // Including the base
  9. class Sprite {
  10. x = 0;
  11. y = 0;
  12. }
  13. // Then you create an interface which merges
  14. // the expected mixins with the same name as your base
  15. interface Sprite extends Jumpable, Duckable {}
  16. // Apply the mixins into the base class via
  17. // the JS at runtime
  18. applyMixins(Sprite, [Jumpable, Duckable]);
  19. let player = new Sprite();
  20. player.jump();
  21. console.log(player.x, player.y);
  22. // This can live anywhere in your codebase:
  23. function applyMixins(derivedCtor: any, constructors: any[]) {
  24. constructors.forEach((baseCtor) => {
  25. Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
  26. Object.defineProperty(
  27. derivedCtor.prototype,
  28. name,
  29. Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
  30. Object.create(null)
  31. );
  32. });
  33. });
  34. }

这种方式对编译器的依赖较低,更多地需要代码的运行时部分和类型部分保持准确的匹配。

限制(Constraints)

Mixin 在 TypeScript 内部是通过代码流分析(code flow analysis)实现的,您可能遇到一些边界情况。

装饰器和 Mixin(Decorators and Mixins)

不可以通过装饰器来应用 Mixin。

  1. // A decorator function which replicates the mixin pattern:
  2. const Pausable = (target: typeof Player) => {
  3. return class Pausable extends target {
  4. shouldFreeze = false;
  5. };
  6. };
  7. @Pausable
  8. class Player {
  9. x = 0;
  10. y = 0;
  11. }
  12. // The Player class does not have the decorator's type merged:
  13. const player = new Player();
  14. player.shouldFreeze;
  15. // Error: Property 'shouldFreeze' does not exist on type 'Player'.
  16. // It the runtime aspect could be manually replicated via
  17. // type composition or interface merging.
  18. type FreezablePlayer = Player & { shouldFreeze: boolean };
  19. const playerTwo = (new Player() as unknown) as FreezablePlayer;
  20. playerTwo.shouldFreeze;

静态属性 Mixin(Static Property Mixins)

这一点更像是一个特性而不是限制。类表达式模式创建的是一个单例(singleton),所以它们并不能映射到类型系统中以支持不同的可变类型。

换一种写法就好了:

  1. function base<T>() {
  2. class Base {
  3. static prop: T;
  4. }
  5. return Base;
  6. }
  7. function derived<T>() {
  8. class Derived extends base<T>() {
  9. static anotherProp: T;
  10. }
  11. return Derived;
  12. }
  13. class Spec extends derived<string>() {}
  14. Spec.prop; // string
  15. Spec.anotherProp; // string