泛型
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 及时提醒。
了解数组方法的泛型
经过上面的介绍,相信大家都对泛型有一定了解了!那么接下来通过带大家看 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] };
同理如下,见图可理解~
type Partial = { [P in keyof T]?: T[P] };
那么大家应该也 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 };
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;
如图所以,成功提取了 IPrint 的参数类型。
同理如下,提取返回值同样理解~
type ReturnType = T extends (...args: any[]) => infer P ? P : any;
提取构造函数参数类型 & 提取实例类型
下述代码可以提取构造函数参数类型~
type ConstructorParameters any> = T extends new (
...args: infer P
) => any
? P
: never;
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.当函数、接口、类需要在多个地方用到某个类型的时候。