介绍
泛型:generics,参数化类型,全称为 **泛型参数**
,我们接下来都简称为泛型 。
学过面向对象语言的小伙伴都知道继承。但是在这里我要说的是:继承不是某一门语言的特性,是某一类语言的特性。哪一类呢?答案是面向对象语言。好了,问题又来了,面向对象语言为什么要实现继承的特征呢?因为继承背后的思想是代码重用/复用/共享,编写的代码可以被许多派生类型的对象所重用。
因为我们要复用代码,所以有了继承。
有时候我们嫌代码的力度太小,想要复用文件,怎么办呢?
import
关键字应运而生。
在Java里面,import
运行后被编译器替换为包的路径限定名。语法 import 包名
帮助我们实现了文件的复用。
在JavaScript里面, import 文件名
运行后帮助我们在作用域链上方建立了一个 module
作用域。
import 帮助我们实现了文件的复用。
但是在下面这种场景下如果我们想要实现算法的复用,怎么办呢?
注意:
- C 语言中代码 = 算法 + 数据结构 。
- Java语言中 代码 = (数据结构 + 算法)
场景1:
写一个函数,这个函数会返回任何传入的值。
eg:不用泛型的话,这个函数可能是下面这样:
function identity(arg: number): number {
return arg;
}
如果我们要编写框架,就要考虑到各种返回值的情况,于是可能就会有这样的代码:
type idBoolean = (arg: boolean) => boolean;
type idNumber = (arg: number) => number;
type idString = (arg: string) => string;
很明显,我们的这些代码逻辑是相同的,但是由于需要,我们不得不写 n
遍这样的逻辑。
有些小伙伴可能会说,我们可以使用any
类型来定义函数:
function identity(arg: any): any {
return arg;
}
使用any
类型会导致这个函数可以接收任何类型的arg
参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。
所以为了解决算法的复用,我们引入了泛型的概念。如下所示:
function identity<T>(arg: T): T {
return arg;
}
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。
接下来我们就可以通过这样的语法调用 identity
函数。
identity<string>('hello');
泛型就是创建类型变量用以接受任意类型。
小结
import
解决了文件的复用。
继承解决了代码的复用。
泛型解决了算法的复用。
泛型是什么?
普通函数要求我们对 值
编程,泛型则要求我们对 类型
编程。
啊?天呐!类型还可以编程?当初笔者也比较迷惑,类型怎样编程,后来随着对TS的深入理解,逐渐清晰了所谓的类型编程
和值编程
。
类型编程
接下来带你体验一下所谓的类型编程。
我们在 TypeScript 中有这样两个操作符 ‘|’ 和 ‘&’。
- ‘|’:取并集运算符。
- ‘&’:取交集运算符。
我们先来看看集合的概念。
文氏图
文氏图用于展示在不同的事物群组(集合)之间的数学或逻辑联系,尤其适合用来表示集合(或)类之间的 “大致关系”,它也常常被用来帮助推导(或理解推导过程)关于集合运算(或类运算)的一些规律。
在文氏图法中,如果有论域,则以一个矩形框(的内部区域)表示论域;各个集合(或类)就以圆/椭圆(的内部区域)来表示。两个圆/椭圆相交,其相交部分表示两个集合(或类)的公共元素,两个圆/椭圆不相交(相离或相切,而实际上在文氏图中相切是没有什么意义的,因为文氏图是以图形的内部区域来表示的)则说明这两个集合(或类)没有公共元素。
比如黄色的圆圈(集合 A)可以表示两足的所有动物。蓝色的圆圈(集合 B)可以表示会飞的所有动物。黄色和蓝色的圆圈交叠的区域(叫做交集)包含会飞且两足的所有动物 —— 比如鹦鹉。 (把每个单独的动物类型想像为在这个图中的某个点)。
(图片来源:https://zh.wikipedia.org/wiki/文氏图)
在TypeScript中,我们的类型就是取自集合的思想。例如: string
类型是所有字符串的集合, number
是所有数字的集合。在 TS 中不仅有这些已经定义好的类型,还有可以提供我们自定义类型的关键字。例如: interface
。我们使用联合类型(A|B)表示集合中的元素属于 A
或者属于 B
。使用交叉类型(A&B)表示集合中的元素既属于 A
又属于 B
。
如果 A = {name: string}
, B = {age: number}
,那么
属于 A
或者属于 B
的元素(A|B)为仅仅包含name或仅仅包含age或包含name和age的对象,既属于 A
又属于 B
的元素(A&B)为包含name和age的对象。
示例1:
在 TypeScript 中编程要注意将文件的
interface cat = {name: string, purrs: boolean};
interface dog = {name: string, barks: boolean};
interface catAndDog = cat & dog;
interface catOrDog = cat | dog;
let a: catOrDog = {name: 'jack',purrs: true,barks: boolean};
a = {name: 'jack', purrs: true};
a = {name: 'jack', barks: true};
let b: catAndDog = {name: 'tom',purrs: true,barks: boolean}
示例2:
假设我们定义了一个person shape:
interface Person {
name: string,
age: number,
phone: string
}
有一个需求是填写表单并统计,其中 name
age
和 phone
是k可选的,但是 age
是可选的。我们可以再造一个接口
interface Person {
name?: string,
age?: number,
phone?: string
}
好了,我们的需求解决了,啊哈。
不过TypeScript为我们提供了一种更好的方式----Partial。我们还可以通过这种方式 Partial<Person>
实现上面的需求。
嗯????Partial帮助我们完成了什么工作呢?怎么就这么突然就帮我们将 Person
的必选项转化为可选项了呢?我们来猜想一下他的实现步骤~
// js伪代码
functon Partial(Type){
// 遍历Type
for(k in Type){
// 将Type中的每个元素转化为可选的
Type[k] = Optional(k);
}
return Type;
}
// 可以看成是上面的函数定义,可以接受任意类型。由于是这里的 “Type” 形参,因此理论上你叫什么名字都是无所谓的,就好像函数定义的形参一样。
type Partial<Type> = { do something }
// 可以看成是上面的函数调用,调用的时候传入了具体的类型 Person
type PartialedPerson = Partial<Person>
接下来看下函数调用和类型调用写法对比。
真实的 Partial
是什么样的呢?
type Partial<T> = {
[P in keyof T]?: T[P];
};
我们之前说类型编程,这里 Partial
的实现就是类型编程的样例。记住这里是类型的运算,所以 keyof
in
?
:
是集合操作符。
keyof
:获取集合所有键的类型,合并为一个字符串字面量类型。假如T
的类型为{a:number,b:string},,则keyof T
为"a" | "b"
。in
:遍历集合中的元素。[P in keyof T]
遍历keyof T
的结果集。?:
:选择性映射。[P in keyof T]?: T[P]
将遍历的结果映射到T[P]
上。
为了方便我们编程,TypeScript 中的对类型操作的语法类似面向对象语言中对值的语法。我们来看看对泛型的操作是不是和函数操作很像:
- 从外表看只不过是
function
变成了type
,()
变成了<>
而已。 - 从语法规则上来看, 函数内部对标的是 ES 标准。而泛型对应的是 TS 实现的一套标准。
泛型的用法就是对类型进行编程。通过 TypeScript 提供的集合操作符对泛型进行编程能够拼接得到任意想要的类型。
但是注意,我们在编写
.ts
文件的时候,使用的是 TypeScript 语法。部分内容遵守ECMAScript规范,但是牵涉到对类型编程的时候ECMAScript规范没有定义,所以是TypeScript自己定义的,所以同一个操作符在不同位置可能有不同的含义。例如中括号[]
,有时候代表数组取值,有时候代表类型键入。其中这样的例子还有很多。
什么时候绑定泛型
声明泛型的位置不仅会限定泛型的作用域,还决定了什么时候为泛型绑定具体的类型。
T
在调用签名中声明(位于签名开始的圆括号前),TypeScript
将在调用 Filter
类型的函数时为T绑定具体的类型。
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter = (array, f) => ......
如果想要把 T
的作用域限定在类型别名 Filter
中,则要求在使用 Filter
的时候显示绑定类型。
type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter = (array, f) => ...... // error
let filter: Filter<number> = (array, f) => ......
通常情况下:
- 函数在被调用时绑定类型
- 接口在实现时绑定类型
- 类在实例化时绑定类型
可以在什么地方使用泛型
可以在任何支持调用值的地方实现泛型。
// 作用域覆盖整个签名
1、type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
// 作用域覆盖函数调用期
2、type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
// 3 是 2 的简写形式
3、type Filter = <T>(array: T[], f: (item: T) => boolean): T[]
// 4 是 1 的简写形式
4、type Filter<T> = (array: T[], f: (item: T) => boolean): T[]
// 具名函数调用签名
5、function Filter<T>(array: T[], f: (item: T) => boolean): T[] { ...... }
泛型别名和泛型推导
泛型推导
在调用函数的过程中,TS能够根据我们传入的值推导出泛型的类型。
// 将T[]的数组映射为U[]类型的数组
function map<T, U>(array: T[], f: (item: T) => U): U[]{
let result = [];
for(let i = 0; i < array.length; i ++){
result[i] = f(array[i])
}
return result;
}
// 调用map函数,经过TS的推导,T的类型是string,U的类型是boolean。
map(['a', 'b', 'c'], _ => _ === 'a');
泛型别名
泛型别名很容易理解。就是在我们的自定义类型中使用泛型,上文中的 Partial<T>
就是泛型别名。我们还可以根据自己的需求定义属于自己的泛型别名。
例如我们定义一个MyEvent类型,描述DOM事件,例如click或mousedown:
type MyEvent<T> = {
target: T;
type: string;
}
使用MyEvent这样的泛型别名时,一定要绑定类型参数,TS无法自动推导。
let event: MyEvent<HTMLButtonElement | null> = {
target: document.querySelector('#myButton'),
type: 'click'
}
使用类型别名构建其他类型。
type TimeEvent<T> = {
MyEvent<T>;
from: Date;
to: Date;
}
在函数签名中使用泛型别名。
function triggerEvent<T>(event: MyEvent<T>): void {
// ...
}
// 调用triggerEvent时传入了一个参数,这个参数默认是MyEvent类型。
// TS发现传给对象target的值为document.querySelector('#myButton'),这意味着 T 为 document.querySelector('#myButton') 类型。
triggerEvent({
target: document.querySelector('#myButton'),
type: 'mouseOver'
})
Promise注意事项
TS 官方定义的 Promise 为:
interface Promise<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}
Promise<T> 是比较经典的泛型别名的使用。
泛型约束
正如文章开头那样,我们可以对函数的参数进行限定。
function t(name: string) {
return `hello, ${name}`;
}
t("lucifer");
如上代码对函数的形参进行了类型限定,使得函数仅可以接受 string 类型的值。那么泛型如何达到类似的效果呢?
type MyType = (T: constrain) => { do something };
还是以 id 函数为例,我们给 id 函数增加功能,使其不仅可以返回参数,还会打印出参数。熟悉函数式编程的人可能知道了,这就是 trace 函数,用于调试程序。
function trace<T>(arg: T): T {
console.log(arg);
return arg;
}
假如我想打印出参数的 size 属性呢?如果完全不进行约束 TS 是会报错的:
function trace<T>(arg: T): T {
console.log(arg.size); // Error: Property 'size doesn't exist on type 'T'
return arg;
}
报错的原因在于 T 理论上是可以是任何类型的,不同于 any,你不管使用它的什么属性或者方法都会报错(除非这个属性和方法是所有集合共有的)。那么直观的想法是限定传给 trace 函数的参数类型应该有 size 类型,这样就不会报错了。如何去表达这个类型约束的点呢?实现这个需求的关键在于使用类型约束。 使用 extends 关键字可以做到这一点。简单来说就是你定义一个类型,然后让 T 实现这个接口即可。
interface Sizeable {
size: number;
}
function trace<T extends Sizeable>(arg: T): T {
console.log(arg.size);
return arg;
}
注意这里的extends不是继承,准确来讲是限制,限制 T 的类型为 Sizeable 的子集。Sizeable 中的属性一定要出现在 T 中出现。
泛型默认类型
泛型在定义的时候可以指定默认类型。
以 MyEvent 来举例
type MyEvent<T> = {
target: T;
type: string;
}
为了给事先不知道 MyEvent 将会绑定何种元素的情况提供便利,我们可以给 T 指定一个默认的类型。
type MyEvent<T = HTMLElement> = {
target: T;
type: string;
}
参考文章
-
你不知道的 TypeScript 泛型 本文中大部分内容来资源此文,衷心感谢作者。
-
TypeScript编程 第一版 中国电力出版社