简介
考虑API接口的可重用性,也就是保证接口不仅能够支持当前的数据类型,同时也能支持未来的数据类型,因此可以考虑使用泛型。
在 TypeScript 中泛型非常常见,尤其是vue3源码中很多地方都使用到了泛型。
函数泛型
在之前对于函数入参和返回值的类型限制可以直接通过具体的类型来约束,但是有时为了让入参和返回值的类型更加灵活,就需要用到泛型来进行优化。
函数泛型的使用:
单个泛型:[函数名]<泛型参数名>
多个泛型:[函数名]<泛型参数名1 | 泛型参数名2>
注意:泛型参数名可以任意写,只要保证在用到泛型的地方,在数量上和使用方式上一致即可。
// 数字类型函数
function fn1(a: number, b: number): Array<number> {
return [a, b]
}
console.log(fn1(1, 2)) //[1, 2]
// 如果想让上面的函数兼容字符串类型的入参和返回值
// 单个泛型
function fn2<T>(a: T, b: T): Array<T> {
return [a, b]
}
console.log(fn2<number>(1, 2)) //[1, 2]
console.log(fn2<string>('a', 'b')) //['a', 'b']
// 多个泛型,在数量上和使用方式上都要一一对应
function fn3<T, U>(a: T, b: T): Array<T|U> {
if(a===b){
return a===b
}else {
return a
}
}
console.log(fn2<number, boolean>(1, 1)) //true
console.log(fn2<number, boolean>(1, 2)) //1
对象字面量泛型
可以使用带有调用签名的对象字面量来定义泛型函数,也就是利用在对象中定义匿名函数,来定义新的函数。
使用:
对象名字 = { <泛型参数名> }
// 使用泛型定义一个对象字面量
// 这里我们的泛型参数名前没有对象名字,可以理解为Oa就是前面的那一部分,
// 在赋值的时候,主要关注的是泛型的名字和数量,而不是变量的名字
let Oa = {
<T>(a: T): T
}
// 创建函数赋值给定义好的对象字面量, 函数在js里面就是一种特殊的对象
Oa = function fn<T>(a: T): T {
return a
}
泛型接口
在上面对象字面量的基础上,我们可以衍生为接口泛型,定义方法和上面对象字面量相似,但往往按照对象字面量的写法只能局限于接口中某个属性和方法中的泛型使用。
如果我们需要在整个接口中都使用到某些泛型,则可以把泛型直接写在接口名字后<泛型参数名>
使用:
[接口名字]<泛型参数名>
// 对象字面量衍生的皆苦泛型,比较局限, 但是针对我们下面的例子的情况是适用的
interface Ia {
<T>(a: T): T
}
// 使用泛型,定义函数类型的接口
interface Ib<T> {
(a: T): T
}
// 创建函数
function fn<T>(a: T): T {
return a
}
// 如果我们将函数赋值给某个变量,该变量的类型就可以使用接口泛型
let fn1: Ia<number> = fn
let fn2: Ib<number> = fn
console.log(fn1(1)) //1
console.log(fn2(1)) //1
泛型类
泛型类和泛型接口相似,泛型类也是使用<>括起泛型类型,跟在类名后面,这样就可以对整个类下面的属性或方法使用相同的泛型。
使用:
类名字<泛型参数名>
// 创建使用了泛型的类
class Ca<T> {
value: T;
constructor(value: T) {
this.value = value
}
set(a: T): void {
this.value = a
}
}
// 实例化类
let ca = new Ca<string>('hello world')
// 测试实例化的类
console.log(ca.value) //hello world
ca.set('hello Ts')
console.log(ca.value) //hello Ts
需要注意的是,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
泛型约束
有的时候使用了泛型定义了一个变量,如果我们想要获取该变量下的某个属性或方法,但是编译器并不能推断该变量类型下是否拥有该属性或方法,所以这时就会ts报警。
因此我们需要使用接口来描述约束条件,通过接口定义我们在这个泛型变量下需要的属性或者方法即可。
// 创建接口
interface Ia {
length: number
}
// 泛型使用关键字extends继承接口类型,这样被泛型T定义的入参下就拥有length属性
// 不会在我们传参为字符串时,ts提示下面没有length属性
function getLength<T extends Ia>(a: T): number {
return a.length
}
console.log(getLength('123')) //3
比如我们把接口 Ia 下的属性length注释后,就会发现 a.length 会有ts报警,如下图。

如果我们给这个函数传递一个没有length属性的值,也会触发ts警报,这样ts的优势就体现出来了。

Keyof和泛型约束
在定义了某个泛型约束后的泛型参数,如果后续其它泛型参数想拥有前者中泛型约束的属性,那么就可以使用Keyof关键字,来将某个泛型变为另一个泛型的子类型。
Keyof关键字主要作用就是获取某个接口泛型下所有的键。
// 创建函数,使用泛型
// 第一个入参就是一个对象,
// 第二个入参使用了keyof和泛型约束,是第一个参数的子属性
function fn<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}
// 定义函数的入参
let obj = {
a: 1,
b: 2,
c: 3
}
// 执行函数
console.log(fn(obj, 'a')) //1
console.log(fn(obj, 'b')) //2
console.log(fn(obj, 'c')) //3