Typescript 致力于支持在不同框架和库中使用的 Javascript 通用模式(common pattterns)。从 Typescript 2.2 开始,mixin 类就是其中一种得到静态类型检查支持的模式。这篇文章会先简单介绍 mixin 是什么,然后通过一些例子来展示如何在 Typescript 中去使用。
Mixin 类是某种实现了特定功能的类。其他的类可以引入 mixin,并可以访问它的方法和属性。如此,mixin 提供了一种基于组合行为的代码重用方式。
Mixin 是一个这样的函数:
1. 接收一个构造函数,
2. 声明一个继承于这个类的构造函数
3. 给这个新类添加成员
4. 然后返回这个类
—— Announcing Typescript 2.2 RC
定义已经清楚了,让我们再深入到代码中一窥究竟。下面代码中有一个 Timestamped
mixin,它通过一个 timestamp
属性来记录对象创建的日期。
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
上面代码包含的信息量相当多。让我们从最顶上的类型别名来庖丁解牛:
type Constructor<T = {}> = new (...args: any[]) => T;
Constructor<T>
类型是一个别名,代表了一个能够构造泛型 T 类型对象的构造函数的签名,并且这个构造函数能够接受任意类型任意数量的参数。它使用了泛型参数默认值(Typescript 2.3 中引入)来明确 T 的类型, 除非被设定为其他类型,否则会默认使用 {}
类型。
接下来让我们看一看 mixin 函数本身:
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
这里我们有一个函数叫 Timestamped
,接收一个 TBase
泛型类型的参数 Base
。注意 TBase
被限定必须和 Constructor
类型兼容,也就是说,这个类型可以构造某种对象。
在这个函数的函数体中,我们创建并返回了一个继承自 Base
的新类。这种语法初看会有点奇怪。在这里,我们创建了一个类表达式而不是类声明,而类声明是定义类更通常的方式。我们的新类定义了一个属性叫 timestamp
,然后立即设置了一个以毫秒为单位的 Unix 时间戳。
注意从 mixin 函数返回的类表达式是一个匿名的类表达式,因为 class
关键字后面没有带名字。和类声明不同的是,类表达式命名不是必须的。你可以选择性地给这个类添加一个类名,它在类中可以被访问并且允许这个类引用自己:
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class Timestamped extends Base {
timestamp = Date.now();
};
}
我们已经介绍了上面的类型别名和 mixin 函数的声明,我们来看看如何在另一个类中使用这个 mixin:
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Create a new class by mixing `Timestamped` into `User`
const TimestampedUser = Timestamped(User);
// Instantiate the new `TimestampedUser` class
const user = new TimestampedUser("John Doe");
// We can now access properties from both the `User` class
// and our `Timestamped` mixin in a type-safe manner
console.log(user.name);
console.log(user.timestamp);
Typescript 编译器理解了我们创建和使用的 mixin。所有的一切都能拥有静态类型,我们因此得到了自动补全和重构相关的工具支持。
现在我们再来看一个稍微更高级的mixin。这次我们会在 mixin 的类中定义一个构造函数:
function Tagged<TBase extends Constructor>(Base: TBase) {
return class extends Base {
tag: string | null;
constructor(...args: any[]) {
super(...args);
this.tag = null;
}
};
}
如果你要在 mixin 类中定义一个构造函数,它必须是一个类型为 any[]
的剩余参数。这么做的原因是,mixin 类不应该和一个特定的类绑定已知的构造函数参数;因此 mixin 应该要支持接收任意类型任意数量的参数。所有的参数都会被传给 Base
的构造函数,然后 mixin 再处理它负责的事情。在我们的例子中,它初始化了 tag
属性。
我们可以和使用 Timestamped
的方式一样去使用 Tagged
mixin:
// Create a new class by mixing `Tagged` into `User`
const TaggedUser = Tagged(User);
// Instantiate the new `TaggedUser` class
const user = new TaggedUser("John Doe");
// We can now assign values to any property defined in either
// the `User` class or our `Tagged` mixin in a type-safe manner.
// TypeScript will type-check those assignments!
user.name = "Jane Doe";
user.tag = "janedoe";
直到现在,我们只给我们的 Mixin 添加了属性。现在我们再看一个实现了两个方法的 mixin:
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActivated = false;
activate() {
this.isActivated = true;
}
deactivate() {
this.isActivated = false;
}
};
}
我们在 mixin 函数返回了一个 ES2015 中的类,这意味着你可以使用所有类语法支持的特性,比如构造函数,属性,方法,getters/setters,静态成员,等等。
再一次,我们可以向下面这样配合 User
类使用 Activatable
mixin:
const ActivatableUser = Activatable(User);
// Instantiate the new `ActivatableUser` class
const user = new ActivatableUser("John Doe");
// Initially, the `isActivated` property is false
console.log(user.isActivated);
// Activate the user
user.activate();
// Now, `isActivated` is true
console.log(user.isActivated);
当你开始组合使用 mixin 的时候,它的灵活性显而易见。一个类可以引入你想要的任意多个 mixin,我们来组合上面介绍的所有 mixin。
const SpecialUser = Activatable(Tagged(Timestamped(User)));
const user = new SpecialUser("John Doe");
现在我并不确定 SpecialUser
类是否真的非常有用,但重点是,Typescript可以理解这个 mixin 组合的静态类型。编译器可以检查所有相关的操作,并在自动补全列表中给出可用成员的建议:
如果和类的继承相比,你会发现其中区别:一个类只能有一个基类。无论在 Javascript 或 Typescript 中,你都不可能继承多个基类。