背景
泛型用于创建可复用的支持多种类型的组件,比如不仅能支持当前的类型,还能支持未来的类型,为大型系统的构建提供一定灵活性,泛有广泛、多种的意思,即泛型可实现对多种类型的支持;泛型是一种已有的概念,除了 TypeScript,同样也存在于其他多种语言中;
先举一个基本的例子,ts 中实现一个加法运算的函数,可以是这样的:
1 | function addFn(arg1: number, arg2: number): number { |
如果后期功能拓展,需要上述函数也具备拼接字符串的功能,即:
1 | function addFn(arg1: string, arg2: string): string { |
但是这样的申明并不与上面已有的申明兼容,即使使用联合类型处理也比较复杂,但是它们的处理逻辑却是一样的,只是类型值换了,重写一个新函数也不符合拓展的初衷,所以需要寻求其他方法;
函数重载
在 ts 中重载是为一个函数提供多个类型定义的操作,使得函数可以根据传参的不同而拥有不同的返回类型;这样,我们就能轻松实现之前例子的拓展需求:
1 | function addFn(arg1: string, arg2: string): string; |
这个例子中,该申明函数与正常函数的区别是:
- 在函数申明的上方又叠加了几个申明表达式,包裹参数类型和返回类型,末尾以分号结束,每一个申明便是一个重载;
- 然后是在函数区块中写处理逻辑,同时包含进上面的几种参数与返回类型的情况;
- 最后调用时就可以传入不同重载所对应的传参类型,并且能通过类型检查,而不在重载定义中的类型则不会通过类型检查;
- 函数在调用时,ts 会在申明的函数重载中自上而下查找第一个匹配的重载签名,最后一个函数签名称为“实现签名”,并不会被调用;
使用重载虽然实现了同时能计算数字和拼接字符串的需求,但是这种写法还是有些复杂,因为参数类型与返回类型具有一定规律性;因此还可以继续寻求更简便的方式;
类型变量
在 ts 中,可以使用泛型来解决上述需求,具体的方式是使用类型变量,顾名思义,ts 包含一个庞大的类型处理系统,有各种使用类型的情景,为了应对一些场景,就需要类型值有像变量一样的变化性,支持赋值与取值操作;
先看一下使用类型变量的具体写法:
1 | function addFn<T>(arg1: T, arg2: T): T { |
这里的写法就比写重载的形式简便了许多;示例中出现了 <T>
这个标识,其中 T
表示类型变量,<>
表示对类型变量的申明,即申明时使用 <>
设置变量,调用时再使用 <>
进行赋值,这样所有用到变量 T
的地方都会被替换为传入的类型值;这里可以发现,泛型就像类型系统中一个针对类型的函数,类型参数就是形参;
调用时可以省略类型赋值的操作是因为上面的场景中 ts 可以利用类型推断机制自动判断出 T
的实际类型值(number
);
由于 T
表示任意类型,所以不能直接访问某些属性或方法:
1 | function fn<T>(arg: T): T { |
如果是复合类型,则可以使用某些固有属性:
1 | function fn<T>(arg: T[]): string { |
类型变量也可以使用其他字母或者单词(通常使用 T),并且可以同时定义多个变量:
1 | function fn<M, My, other>(arg: M): M { |
泛型接口
除了函数,泛型也可以在接口中使用,例如:
1 | interface IGeneric { |
或者是针对整个接口的泛型:
1 | interface IGeneric<T> { |
泛型类
针对类定义类型变量时,类的所有非静态成员都可以使用该变量:
1 | class CS<T> { |
泛型约束
在一些场景中,可能并不期望类型变量的值太泛,而是需要具有某些属性或方法,这时就可以使用 extends
关键字对类型加一些约束,这和类中的继承用途也有些类似;例如:
1 | interface IObj { |
结合其他 ts 特性,也能表示一些特殊情形,比如下例表示函数第二个参数,需要是第一个参数对象的属性名之一,keyof
关键字为索引查询操作符,keyof T
表示 T
的所有属性构成的联合类型:
1 | function fn<T, K extends keyof T>(obj: T, key: K) { |