同事问我:TypeScript中的new () => Xxx 究竟是什么惊喜?

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

起因

虽然说之前有断断续续的学习TypeScript(后面均用TS简称)的经历,但最近才真正意义上尝试着使用进行一个项目开发。
在开发过程中,我的同事阿洋很快就遇到了这样一个问题:

我希望有一个方法,我传入类,它可以根据我传入的类进行实例化;

如果用原生JS来实现,那就是如下:

function test(fn){
  const _instance = new fn();
}
复制代码

由于早年搞过一些JAVA开发,我首先脑海里想出来了JAVA对应的语法:

  private void test(Class clazz) throws Exception {
    Object _instance = clazz.newInstance();
  }
  // 以下为使用
  test(Cat.class)
复制代码

然后我本着同事的问题就是我的问题的初心,迅速浏览了一遍TS官方文档-基础类型一节。 嗯?当时我就发现事情并不简单。
基础类型中并没有TypeClass一类的类型,也就是说,好像并不支持直接进行Class的传递
应该不至于,如果不支持这个特性,估计TS早就喷的一塌糊涂了;
那么在TS中,这块的语法应该是怎样的呢?

814268e3gy1feeba43bswg205a04i7hl.gif

找到解法

由于刚接触TS,对其语言特性了解有限,我决定阅读开源项目的源码进行参考学习。
很快,我在nest的源码中,找到了类似的场景,并且找到了实实在在的解决办法;

export interface Type<T = any> extends Function {
  new (...args: any[]): T;
}
复制代码

用法如下:

function test(fn: Type):void {
  const a = new Type();
}

// 以下为使用
test(Cat);
复制代码

甚至,它还能支持继承;

function test(fn: Type<Animal>):void {
  const a = new Type();
}

class Animal {
  ...
}

class Cat extends Animal {
  ...
}

// 以下为使用
test(Cat);
复制代码

OK,找到了可以用的代码片段,但是这段代码片段是什么意思呢?
它们又用到的TS官方文档中的哪些知识点呢?

理解&学习

接下来,我需要理解一下,为什么这段代码能实现TS里的类似Java里Class的表现?

export interface Type<T = any> extends Function {
  new (...args: any[]): T;
}
复制代码

首先,它的定义是interface,所以需要看的第一个点,是interface;

一、 函数类型接口

在官方文档中,有这样一段描述:

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}
复制代码

可以确定,上面的方法所定义的,是一个函数类型接口,它支持传入的是一个函数类型;
让我们暂时抛开其他难以理解的部分,理解这个接口最核心的部分,经过删减,如下:

interface Type{
  (...args: any[]): any;
}
复制代码

一个真正意义上的函数类型接口,该接口的用法是指:传入的参数需要是一个函数,且需要对其参数进行类型检查;
而在本例子中,参数为 ...any[],也就是未做类型和数量的限制;说明它对方法的参数未做要求; 在此基础上,我们需要一点点拼凑出它完整的含义;那么下一个关键词便是new;

二、 关键词:new

在源代码中,对方法的描述,实际上是比示例里最简单的函数类型接口的示例,多了一个new关键词的;

export interface{
  new (...args: any[]): any;
}
复制代码

在官方接口里,并没有找到直接描述new ()的内容点,直接进行一个StackOverFlow,在该链接中,有人描述如下:

new() describes a constructor signature in typescript. What that means is that it describes the shape of the constructor. For instance take {new(): T; }. You are right it is a type. It is the type of a class whose constructor takes in no arguments.

翻译并且删减一哈:

new()用来描述一个构造函数;

也就是说,该接口不仅仅是指定了一个函数类型接口,而且指定了该函数类型接口,需要是一个构造器函数

OK,关于new的这个知识点现在清楚了,那继续往下看;

上面代码里还用到了extends 关键词,所以又涉及到了第二个知识点:继承接口

三、 继承接口

首先,让我们明确一个知识点,Function在TS里,究竟是什么?
答案是:接口;

interface Function {
    /**
     * Returns the name of the function. Function names are read-only and can not be changed.
     */
    readonly name: string;
}
复制代码

所以,这里用到的是继承接口的知识点,Type是一个继承了Function的接口,它是一个函数,且众所周知,TS里的class就是方法,且在编译后就是一个构造函数;

  class A {
    constructor(){}
  }
  const b = A instanceof Function; // true
复制代码

ok,对于这里的理解我们再进一步理解;

interface Type extends Function{
  (...args: any[]): any;
}
复制代码

这代表了一个构造器函数接口

四、 泛型

export interface Type<T = any> extends Function {
  new (...args: any[]): T;
}
复制代码

最后一个知识点,就在于其中的T是什么了。
按上面的写法,如果每次都返回any,对于TS而言,这体验显然是糟糕的;那么要怎么描述返回的类型呢?
显然,只能是用泛型了;
在官方文档中,如此描述泛型:

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

而本例,用到的则是泛型接口:

我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
复制代码

小节

小小的一段代码中,其实包含了以下几个知识点:

  • 函数类型接口
  • new实例化方法
  • 继承接口
  • 泛型

在逐一了解以上这些知识点后,对该写法也会有一个更为清晰的认识;

Guess you like

Origin juejin.im/post/7032280251119960078