TypeScript:构造器接口模式

500 阅读4分钟

如果你用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 这个抽象类需要由其他类来实现。在这个例子中,由AFilterBFilter 。到目前为止,一切都很好。古典类型的工作方式就像你在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构造函数的接口。这里有所有的静态属性,以及构造函数本身。构造函数返回一个实例:IFilterIFilter 包含实例端的类型信息。所有我们声明的函数。

通过拆分,我们后续的类型划分也变得清晰了许多。

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');
}
  1. 我们将FilterConstructors添加到我们的地图中。这就意味着我们只能添加那些采购了所需对象的类。
  2. 我们最终想要的是一个IFilter 的实例。这就是构造函数在被调用时返回的new

我们的代码再次编译,我们得到了所有我们想要的自动完成和工具。甚至更好。我们无法在地图上添加抽象类。因为它们不能产生一个有效的实例。

// 💣 Cannot assign an abstract constructor 
//    type to a non-abstract constructor type.
filterMap.set('notworking', FilterItem)