泛型
A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems.
In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
我们工作的大部分内容是构建组件,定义有效、一致并且可复用的API很重要。组件能够处理当前的数据,又能考虑兼容很多未来的数据,这样的组件能提高工作效率,也能在构建软件系统的时候提供非常灵活的能力。
基本使用
通过泛型可以定义通用的数据结构,增加 TypeScript 代码中类型的通用性。
先看一个具体的例子,感受一下泛型的应用。
首先定一个 log 函数,功能很简单把传入的参数直接 return 就行,函数参数类型是 string,那么返回值也是 string 类型。
1 | function log(arg: string): string { |
当其他地方也想使用这个函数,但是参数入参数类型是 number,这个时候我们也许可以这么做:
1 | function log(arg: string | number): string | number { |
当有更多的地方要使用这个函数的时候,那这个函数的参数类型定义和返回值类型定义将会变得无比冗长,或者可能就直接使用 any 来解决,当使用 any 的时候就失去了使用 TS 最初的初心了。
这个时候泛型出现了,能解决输入输出一致的问题,我们可以这样写:
1 | function log<T>(arg: T): T { |
这个 log 函数通过泛型来约束输入输出一致性的问题,把动态的泛型类型抛给函数的使用者,我们只需要保证输入输出的一致性就可以,还能支持任何类型。泛型中的 T 就像一个占位符,或者说一个变量,在使用的时候把定义的类型像参数一样传入就可以了。
我们在使用的时候可以有两种方式指定类型。第一,是直接定义要使用的类型,第二,是默认 TS 的类型推断,TS自动推导出要传入的类型:
1 | log<string>('log') // 定义 T 为 string |
在 JS 中对于一个函数入参,可以使用默认参数来简化当没有传参数时候的默认值,在 TS 中我们可以这样使用:
1 | function log<T = string>(arg: T): T { |
当没有传泛型参数的时候 T 的默认类型是 string 类型,就如果 JS 中的函数默认参数类似的用法
当函数中有多个参数的时候可以这样使用:
1 |
|
通过在泛型定义多个对应位置的类型就可以获取到相应的泛型传参来对输入输出做一些处理。
泛型不仅可以很方便地约束函数的参数类型,还可以用在函数执行副作用操作的时候。发送请求是我们使用的很多的操作,我们会有一个通用的发送请求的异步方法,请求不同的 url 会返回不同的类型数据,那么我们可以这样使用。
1 | function request<T>(url: string): Promise<T> { |
这个时候返回的数据 TS 就会识别出 res 的类型,对解下来的代码编写会有很大帮助。
应用
上面的一些小例子,我们对泛型有了一些了解,在平时我们可以这样使用。
定一个栈,有出栈和入栈两个方法,规定了出栈和入栈的元素类型必须一致。
1 |
|
在使用的使用传入类型生成实例,在调用对应的出栈和入栈方法的时候入参数类型不对就会报错,从而约束了栈的元素类型。
1 |
|
1 | function request<T>(url: string): Promise<T> { |
rInfo 的 address 的类型就是 string,让 interface 更灵活。
Pck 是 TS 内置的函数,作用是挑选出对象类型 T 中 U 对应的属性和类型,创建一个新的对象类型。
1 | type Pick<T, K extends keyof T> = { |
与Pick的功能是互补的,挑选出对象类型 T 中不在 U 中的属性和类型,创建一个新的对象类型。
1 |
|
总结
泛型,从字面上来理解,就是一般的,广泛的,具有通用性的。
泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。
泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出。
extends
本文主要整理 extends 关键字在 typescript中的相关用法,平时在看一些复杂的 TS 类型的时候经常会看到使用 extends 这个关键字
继承类型
TS 中的 extends 关键字第一个用法可以理解成 JS 中相似的用法继承类型。
1 | interface IName { |
以上示例中,IName 和 IGender 两个接口,分别定义了 name 属性和 gender 属性,IPerson 则使用extends 关键字多重继承的方式,继承了 IName 和 IGender,同时定义了自己的属性age,此时 IPerson 除了自己的属性外,还同时继承了 IName 和 IGender 的属性。
条件判断
When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).
当 extends 左边的类型可以赋值给右边的类型时,你会在第一个分支中获取获得这个类型(true),否你会在第二个个分支中获得这个类型(false)
#####普通条件类型
先来直接看个例子
1 | // 示例1 |
extends 的条件判断和 JS 中的三元表达式很类似,如果问号前面的条件为真就把 string 类型赋值给 A,否则就把 number 类型赋值给 A。那么问号前面的条件判断真假的逻辑是什么呢?就像上面的那段英文描述一样,当extends 左边的类型可以赋值给右边的类型的时候,就会真,否则为假。
在上面的例子中,IDog 是 IAnimal 的子类,子类比父类的限制更多,如果能满足子类的条件约束,就一定能满足父类的条件约束,IDog 类型的值可以满足 IAnimal 类型,判断结果为真,Test 的类型为 string。
再来一个例子:
1 |
|
这个例子,代入上面的解法来看就是,能满足 I2 类型约束的值也满足 I1 类型约束,判断结果为真,Test 的类型为 string。
多看几个例子:
1 | type Test1 = 'x' extends 'x' ? "true" : "false"; // "true" |
按照上面的解释能够很好的解释出最后的结果。
###分配条件类型
再多看几个例子:
1 | type Test1 = 'x' extends 'x' ? string : number; // string |
这里就先把最后的结果直接给出来了,看到 Test1、Test2 和 Test4 还能理解,但是 Test3 的结果为什么就是 string |nunber这个类型了呢?同样的按照泛型传参数,按照直觉来说,Test3 和 Test2 应该是一样的结果,为什么结果差异这么大呢?
这里导致结果和直觉不一样的原因就是所谓的分配条件类型。
When conditional types act on a generic type, they become distributive when given a union type
当 extends 前面的参数是一个泛型类型,当传入的参数是一个联合类型的时候,就是使用分配律计算最后的结果,分配律就是我们从数学中学到的分配律。把联合类型中的每个类型代入条件判断得到每个类型的结果,再把每个类型的结果联合起来,得到最后的类型结果。
那么就可以按照这个解法来代入 Test3 的解释原因:
extends 前面的参数 T 是泛型参数,Test3 中 泛型代入的的 x | y这个联合类型,这个时候就触发了分配条件类型,来使用分配律
1 | 'x' extends 'x' ? string : number; // string |
按照分配条件来看最后的结果恍然大悟,总之要触发分配条件类型要满足两个条件,第一,extends 前面的参数是泛型类型,第二,参数是联合类型。
条件分配类型是系统默认的行为,那么在某些需求不想要出发条件分配类型应该怎么办呢?
看下面的例子:
1 | type P<T> = [T] extends ['x'] ? string : number; |
这个使用使用了[]这个符号把泛型类型参数包起来,这个时候 extends 前面的参数就变成这个样子[‘x’ | ‘y’],不满足触发分配条件类型的条件,按照普通条件来判断,得到最后的结果为 number。
never
来个例子看看:
1 | // never是所有类型的子类型 |
上面直接给出了最后的结果,但是为什么看起来 Test2 最后的结果又和直觉中不太一样,never 不是联合类型,直接代入条件类型之后,按理来说 Test2 和 Test1 的结果应该一样才对。
事实上,never 被认为是空的联合类型,也就是没有任何项的联合类型,所以还是满足上面的分配条件类型,因为没有任何联合项可以分配,所以P
按照上面的条件,可以这样子来阻止分配条件类型。
1 | type P<T> = [T] extends ['x'] ? string : number; |
应用
Exclude
1 | type Exclude<T, U> = T extends U ? never : T; |
Exclude 是 TS 内置的应用方法,作用是从第一个联合类型参数 T 中,把第二个联合类型参数 U 出现的联合项去掉。
1 | type Test = Exclude<'A' | 'B', 'A'> // 'B' |
其实就是应用了分配条件类型:
1 | type Test = Exclude<'A', 'A'> | type Test = Exclude<'B', 'A'> |
Extract
1 | type Exclude<T, U> = T extends U ? T : never; |
Extract 是 TS 内置的应用方法,作用是从第二个联合类型参数 U 中,把第一个联合类型参数 T 出现的联合项提取出来。
1 | type Test = Exclude<'A' | 'B', 'A'> // 'A' |
其实就是应用了分配条件类型。
1 | type Test = Exclude<'A', 'A'> | type Test = Exclude<'B', 'A'> |
总结
在 typescript 中的 extends 关键字主要用法,就是继承类型,结合三元表达式来完成更多的类似函数的应用方法,在三元表达式中还要注意分配条件类型的应用。