每一种编程语言都有高效处理重复概念的工具。在 TypeScript 中,泛型就是这类工具之一。
泛型是什么
泛,generic 有通用之意。也就是说,用抽象的类型替代具体的类型,在编写和编译代码时并不需要知道其实际类型是什么。比如一些函数可以对多种类型运行相同的逻辑。
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
我们都知道在 TypeScript 中需要给函数参数和返回值指定参数,但一些函数的参数或返回值的类型对于函数具体逻辑来说其实不太重要。
比如我想获取数组的首位元素,我只需要明确参数 arr 是一个数组就可以了,但它是什么类型数据的集合,我其实不太关心。通过泛型,我们就不用为不同的类型重写相同的逻辑。
其实你早就用过泛型了!下面这组代码是 TypeScript 中数组的声明片段。对,泛型不仅可以替代函数中的类型,还可以用在接口和类中。
interface Array<T> {
fill(value: T, start?: number, end?: number): this;
// ……
}
其实创建了一个数组就具体化了数组接口中的泛型。下面代码中 nums 的类型既是 number[],也是 Array<number>。
const nums = [1, 2, 3];
泛型约束
泛型不是 any
我们知道 TypeScript 存在 any 类型,代表任何类型。既然类型对于逻辑不太重要,为什么不直接使用 any 呢?其实这样的代码也是可以编译通过的。
function getFirstElement(arr: any[]): any {
return arr[0];
}
any 与泛型的区别在于泛型其实是有类型的,这意味着泛型变量要遵守类型约束。
比如上面的例子,如果直接返回 0 是编译不过的。这是因为我们声明 getFirstElement 返回值的类型是 T,而实际是 number。
function getFirstElement<T>(arr: T[]): T {
return 0; // Type 'number' is not assignable to type 'T'.
}
也就是说 T 可以是 number,但 number 不能代表 T。这就是泛型约束,对于 getFirstElement 来说,约束返回值的类型需要和 arr 元素类型一致。这种约束的好处是编译器可以推断出 firstNum 的类型一定是 number。
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const nums = [1, 2, 3];
const firstNum = getFirstElement(nums);
约束对象字段
泛型约束不止于此,其还可以用来约束对象的字段。比如,下面的代码中就确保了参数有 length 字段,也就是所谓“鸭子类型”。
type ArrLike = {
length: number;
}
function getLength<T extends ArrLike>(arrLike: T): number {
return arrLike.length;
}
这种约束在 TypeScript 中有个重要应用,那就是 Partial。Partial<T> 就是将类型 T 中所有字段变成可选的类型。
type Partial<T> = {
[P in keyof T]?: T[P];
};
首先声明 Partial<T> 是一个对象,其键值是 [P in keyof T]。keyof 是 TypeScript 中的操作符,用于获取特定类型的所有键值。也就是说 Partial<T> 中所有的键值必须来自 T 的键值集合,其值必须与 T 对应键的值相同。关键在于 ?,它代表字段是可选的。即 Partial<T> 就是将类型 T 中所有字段变成可选的类型。
这有什么用吗?比如我们要更新对象中的某几个字段的值,就可以将 updates 的类型声明为 Partial<T>,也就是可选地更新 original。
function update<T>(original: T, updates: Partial<T>): T {
return { ...original, ...updates };
}
结语
泛型在 TypeScript 提供的工具中还有很多应用,比如 Map、Set、Record 等等都含有类型参数。
我认为泛型是用来降代码重用的一项重要技术。虽说 TypeScript 只是 JavaScript 的一种编译型超集,泛型并不会影响运行性能,但过早地优化也是不可取的。有时业务就是那么简单,非泛型版本的代码完全可以应对,可读性也较泛型版本高出一大截。