如果你用TypeScript做传统的OOP,TypeScript的结构特性有时可能会妨碍你。例如,请看下面的类层次结构。
abstract class FilterItem {
constructor(private property: string) {}
someFunction() { /* ... */ }
abstract filter(): void;
}
class AFilter extends FilterItem {
filter() { /* ... */ }
}
class BFilter extends FilterItem {
filter() { /* ... */ }
}
FilterItem 这个抽象类需要由其他类来实现。在这个例子中,由AFilter 和BFilter 。到目前为止,一切都很好。古典类型的工作方式就像你在Java或C#中习惯的那样。
const some: FilterItem = new AFilter('afilter'); // ✅
不过,当我们需要结构信息时,我们就会离开传统的OOP领域。比方说,我们想根据从AJAX调用中得到的一些标记来实例化新的过滤器。为了让我们更容易选择过滤器,我们将所有可能的过滤器存储在一个地图中。
declare const filterMap: Map<string, typeof FilterItem>;
filterMap.set('number', AFilter)
filterMap.set('stuff', BFilter)
该地图的泛型被设置为一个字符串(用于来自后台的token),以及补充FilterItem 的类型签名的所有内容。我们在这里使用typeof 关键字,以便能够将类添加到地图中,而不是对象。毕竟,我们想在事后将它们实例化。
到目前为止,一切都像你所期望的那样工作。当你想从地图中获取一个类并用它创建一个新的对象时,问题就出现了。
let obj: FilterItem;
const ctor = filterMap.get('number');
if(typeof ctor !== 'undefined') {
obj = new ctor(); // 💣 cannot create an object of an abstract class
}
这是个什么问题?TypeScript此时只知道我们拿回了一个FilterItem ,而我们不能实例化FilterItem 。由于抽象类混合了类型信息和实际语言(这是我试图避免的),一个可能的解决方案是转移到接口来定义实际的类型签名,并能够在之后创建适当的实例。
interface IFilter {
new (property: string): IFilter;
someFunction(): void;
filter(): void;
}
declare const filterMap: Map<string, IFilter>;
注意new 关键字。这是TypeScript定义构造函数类型签名的一种方式。
大量的💣现在开始出现了。无论你把implements IFilter 命令放在哪里,似乎没有任何实现能满足我们的契约。
abstract class FilterItem implements IFilter { /* ... */ }
// 💣 Class 'FilterItem' incorrectly implements interface 'IFilter'.
// Type 'FilterItem' provides no match for the signature
// 'new (property: string): IFilter'.
filterMap.set('number', AFilter)
// 💣Argument of type 'typeof AFilter' is not assignable
// to parameter of type 'IFilter'. Type 'typeof AFilter' is missing
// the following properties from type 'IFilter': someFunction, filter
这里发生了什么?似乎无论是实现,还是类本身,似乎都不能得到我们在接口声明中定义的所有属性和函数。为什么呢?
JavaScript类很特别。它们不仅有一个我们可以轻松定义的类型,而且有两个类型!静态方面的类型,还有一个类型是我们可以定义的。静态方面的类型,以及实例方面的类型。如果我们把我们的类转译成ES6之前的样子,可能会更清楚:一个构造函数和一个原型。
function AFilter(property) { // this is part of the static side
this.property = property; // this is part of the instance side
}
// instance
AFilter.prototype.filter = function() {/* ... */}
// not part of our example, but instance
Afilter.something = function () { /* ... */ }
一个类型用来创建对象。一个类型用于对象本身。所以让我们把它拆开,为它创建两个类型声明。
interface FilterConstructor {
new (property: string): IFilter;
}
interface IFilter {
someFunction(): void;
filter(): void;
}
第一个类型FilterConstructor 是构造函数的接口。这里有所有的静态属性,以及构造函数本身。构造函数返回一个实例:IFilter 。IFilter 包含实例端的类型信息。所有我们声明的函数。
通过拆分,我们后续的类型划分也变得清晰了许多。
declare const filterMap: Map<string, FilterConstructor>; /* 1 */
filterMap.set('number', AFilter)
filterMap.set('stuff', BFilter)
let obj: IFilter; /* 2 */
const ctor = filterMap.get('number')
if(typeof ctor !== 'undefined') {
obj = new ctor('a');
}
- 我们将
FilterConstructors添加到我们的地图中。这就意味着我们只能添加那些采购了所需对象的类。 - 我们最终想要的是一个
IFilter的实例。这就是构造函数在被调用时返回的new。
我们的代码再次编译,我们得到了所有我们想要的自动完成和工具。甚至更好。我们无法在地图上添加抽象类。因为它们不能产生一个有效的实例。
// 💣 Cannot assign an abstract constructor
// type to a non-abstract constructor type.
filterMap.set('notworking', FilterItem)