泛型

TypeScript 中泛型设计的目的是使在成员之间提供有意义的约束,为代码增加抽象层和提升可重用性。泛型可以应用于 Typescript 中的函数(函数参数、函数返回值)、接口和类(类的实例成员、类的方法)。

简单示例

先来看这个如果平常我们写函数的参数和返回值类型可能会这么写~约束了函数参数和返回值必须为数字类型。

function identity(arg: number): number {  return arg;}

那么问题来了。如果我要参数和返回值类型限定为字符串类型的话,又改成这么写。

function identity(arg: string): string {  return arg;}

不科学呀!当函数想支持多类型参数或返回值的时候,上述写法将变得十分不灵活。于是泛型就闪亮登场了!

考虑以下写法:

function identity(arg: T): T {  return arg;}
function identities(arg1: T, arg2: U): [T, U] {  return [arg1, arg2];}

使用泛型后,可以接受任意类型,但是又完成了函数参数和返回值的约束关系。十分灵活~可复用性大大增强了!

泛型约束

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 给泛型加上约束。

interface ILengthwise {  length: number;}function loggingIdentity(arg: T): T {  console.log(arg.length);  return arg;}

其实泛型我们在 React 组件里也很常见(说不定大家觉得很眼熟了),用泛型确保了 React 组件的 Props 和 State 是类型安全的~

interface ICustomToolProps {  // @TODO}interface ICustomToolState {  // @TODO}class CustomTool extends React.Component {  // @TODO}

所以大家看上面的 ICustomToolProps、ICustomToolState 其实也是泛型。应用在类上面的泛型语法简化如下示例:

class Directive {  private name: T;  public getName(): T {    return this.name;  }  // @TODO}

当使用泛型时,一般情况下常用 T、U、V 表示,如果比较复杂,应使用更优语义化的描述,比如上述 React 组件示例。

实践一下

比如说设计一个指令管理者对象~用来管理指令

enum EDirective {  Walk = 1,  Jump = 2,  Smile = 3}class DirectiveManager {  private directives: Array = [];  add = (directive: T): Array => {    this.directives = this.directives.concat(directive);    return this.directives;  };  get = (index: number): T => {    return this.directives[index];  };  shift = (): Array => {    this.directives = this.directives.slice(1);    return this.directives;  };  // @TODO}

初始化一个指令管理者的实例。给定泛型为 number 类型。




typescript 获取类名 typescript get_typescript get方法


微信图片_20201113105046.jpg

可以发现指令管理者对象成功被限定类型,如果传参类型错误,会被 TypeScript 及时提醒。

了解数组方法的泛型

经过上面的介绍,相信大家都对泛型有一定了解了!那么接下来通过带大家看 JavaScript 数组方法的泛型来加深理解~

我们来阅读以下数组对象的属性以及方法的泛型(我抽取了一部分,希望大家不要觉得代码过长,就略过不读,我觉得也是换一种方式熟悉 JavaScript 语法的一种方式~)

interface Array {  length: number;  [n: number]: T;  reverse(): T[];  shift(): T;  pop(): T;  unshift(...items: T[]): number;  push(...items: T[]): number;  slice(start?: number, end?: number): T[];  sort(compareFn?: (a: T, b: T) => number): T[];  indexOf(searchElement: T, fromIndex?: number): number;  lastIndexOf(searchElement: T, fromIndex?: number): number;  every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;  some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;  forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;  map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];  filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[];  splice(start: number): T[];  splice(start: number, deleteCount: number, ...items: T[]): T[];  concat(...items: U[]): T[];  concat(...items: T[]): T[];  reduce(    callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T,    initialValue?: T  ): T;  reduce(    callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,    initialValue: U  ): U;  reduceRight(    callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T,    initialValue?: T  ): T;  reduceRight(    callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,    initialValue: U  ): U;}

相信大家对数组方法都十分熟悉了~下面将带大家稍微看一下部分方法

shift/pop & push/unshift

shift(): T;pop(): T;unshift(...items: T[]): number;push(...items: T[]): number;

平时大家可能会混淆几个方法。但是看了它们的函数签名后,是否觉得一目了然。push/unshift 方法调用后返回时数字类型,也就是其数组长度。而 shift/pop 方法调用后返回了弹出的元素,

forEach & map

forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];

这两个方法很值得一说,因为两者都具备遍历的特征,所以常见很多同学们混用这两个方法,其实大有讲究。看到 forEach 的方法其实是返回 void 的,而在map 方法里,最终是将 T[] 映射成了 U[]。所以呢,一言以蔽之,forEach 一般用来执行副作用的,比如持久的修改一下元素、数组、状态等,以及打印日志等,本质上是不纯的。而 map 方法用来作为值的映射,本质上是纯净的,在函数式编程里十分重要。

concat

splice、concat、reduce、reduceRight 这些方法基本都重载了两次,也就明显告诉我们这些方法是有多种传参调用方式的。

比如concat(...items: U[]): T[];这里使用到了上述和大家介绍的泛型约束,意思为可以传递多个数组元素。下面紧跟着的concat(...items: T[]): T[];则告诉我们也可以传递多个元素。两个函数签名都告诉我们函数返回一个数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(如果参数是数组)或参数本身(如果参数不是数组)。它不会递归到嵌套数组参数中。

映射类型

有时候我们有从旧类型中创建新类型的一个需求场景,TypeScript 提供了映射类型这种方式。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性

比如我们将每个属性成为 readonly 类型,如下

type Readonly = { readonly [P in keyof T]: T[P] };


typescript 获取类名 typescript get_typescript get方法_02


2.jpg

同理如下,见图可理解~

type Partial = { [P in keyof T]?: T[P] };


typescript 获取类名 typescript get_typescript get方法_03


3.jpg

那么大家应该也 get 到下述代码的意图了~

type Nullable = { [P in keyof T]: T[P] | null };

扩展一下可以写任意的映射类型来满足自己的需求场景~

enum EDirective {  Walk = 1,  Jump = 2,  Smile = 3}type DirectiveKeys = keyof typeof EDirective;type Flags = { [K in DirectiveKeys]: boolean };


typescript 获取类名 typescript get_泛型_04


4.jpg

type Pick = { [P in K]: T[P] };type Record = { [P in K]: T };

条件类型中的推断

infer 表示在 extends 条件语句中待推断的类型变量。

在条件类型的 extends 语句中,我们可以用 infer 声明一个类型变量,然后在其分支语句中使用该类型变量。如果不懂,没有关系,请继续看下面的例子~

提取函数参数 & 提取函数返回值

该语句中的(param: infer P),为函数首个参数推断声明了一个类型变量 P,如果泛型 T 是一个函数,则根据之前的类型变量 P,提取其推断的函数参数并返回,否则返回原有类型。

type ParamType = T extends (param: infer P) => any ? P : T;


typescript 获取类名 typescript get_泛型_05


5.jpg

如图所以,成功提取了 IPrint 的参数类型。

同理如下,提取返回值同样理解~

type ReturnType = T extends (...args: any[]) => infer P ? P : any;

提取构造函数参数类型 & 提取实例类型

下述代码可以提取构造函数参数类型~

type ConstructorParameters any> = T extends new (  ...args: infer P) => any  ? P  : never;


typescript 获取类名 typescript get_类型变量_06


6.jpg

T extends new (...args: any[]) => any这里用到了泛型约束,new (...args: infer P)这一句将参数推断声明为类型变量 P。剩余的还是一样的理解~

下述提取实例类型(和提取构造函数参数类型小有不同同学们自己发现一下)

type InstanceType any> = T extends new (...args: any[]) => infer R  ? R  : any;

其他常用的条件推断

剩余的列举一些比较实用的,参照上述方式理解,同学们如若感兴趣,可自行谷歌~

提取数组子元素

type Flatten = T extends (infer U)[] ? U : T;

提取Promise值

type Unpromisify = T extends Promise ? R : T;

Tuple 转 Union

type ElementOf = T extends Array ? E : never;

Union 转 Intersection

type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((  k: infer I) => void)  ? I  : never;

什么时候使用泛型

1.当函数、接口、类是接受多类型参数的时候,可以用泛型提高可重用性。
2.当函数、接口、类需要在多个地方用到某个类型的时候。