TypeScript 泛型入门:从类型参数到约束与默认值
在日常开发中,我们常常要写一些“形状相同、类型不同”的逻辑:取数组的第一项、交换两个变量的值、把一个对象包装成统一的响应结构。如果为每种类型各写一个函数,代码会迅速膨胀;如果全部用 any,又等于放弃了类型检查。泛型就是为了解决这个矛盾而存在的:它让类型也可以像值一样被“参数化”,在调用时才确定。
一、从一个最简单的函数说起
先看最常见的入门例子 identity:
function identity<T>(value: T): T {
return value;
}
const n = identity(42); // T 被推断为 number,n 的类型是 number
const s = identity("hello"); // T 被推断为 string
const arr = identity([1, 2, 3]); // T 被推断为 number[]
尖括号里的 T 就是类型参数,习惯上用大写字母表示。调用 identity 时可以显式传入类型,也可以让编译器根据实参自动推断;大多数情况下,推断就足够了,写起来更简洁。
如果不用泛型,identity 的参数只能写成 any:
function identityAny(value: any): any {
return value;
}
两者看起来都能跑,但返回值类型完全不同:identity(42) 返回的是 number,可以直接调用数值方法;而 identityAny(42) 返回的是 any,类型检查器不会再帮你发现任何错误。泛型的意义正在于此:让“输入什么类型,就返回什么类型”这种关系能够被类型系统表达出来。
二、泛型不止能用在函数上
类型参数同样可以出现在接口、类型别名和类里。比如一个通用的响应包装:
interface ApiResult<T> {
code: number;
message: string;
data: T;
}
const userResult: ApiResult<{ name: string; age: number }> = {
code: 0,
message: "ok",
data: { name: "Tom", age: 18 },
};
再比如一个键值对类型,两个类型参数各自独立:
type Pair<K, V> = {
key: K;
value: V;
};
const entry: Pair<string, number> = { key: "score", value: 98 };
泛型类也是同样的道理,常见的栈结构就是很好的例子:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("a"); // 报错:string 不能赋给 number
一旦实例化时确定了 T,整个类的所有方法都会围绕这个类型工作,写错类型在编译期就会被拦下来。
三、泛型约束:给类型参数划定范围
有时候我们希望类型参数不是“任意类型”,而是“满足某些条件的任意类型”。这时就要用到约束,关键字是 extends。
最常见的场景是访问某个属性。下面的函数想取参数的 length,但如果 T 是任意类型,编译器无法保证它一定有 length:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // 报错:number 没有 length 属性
约束并不缩小泛型的适用面,而是把“调用者需要知道的前提”写进类型里。
另一个高频用法是 keyof 约束,它可以保证第二个参数一定是第一个参数对象的合法属性名:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Tom", age: 18 };
const userName = getProperty(user, "name"); // string
// getProperty(user, "gender"); // 报错:gender 不是 user 的属性
如果不用这个约束,key 就只能写成 string,返回值也只能是 any,写错属性名要到运行期才暴露。
四、默认值:让常用类型可以省略
类型参数也可以像函数参数一样设置默认值。当调用方不传类型实参时,就使用默认值:
interface Page<T = string> {
list: T[];
total: number;
}
const page1: Page = { list: ["a", "b"], total: 2 };
const page2: Page<number> = { list: [1, 2], total: 2 };
在函数里同样适用:
function createArray<T = string>(length: number, value: T): T[] {
return new Array(length).fill(value);
}
createArray(3, "x"); // string[]
createArray<number>(3, 1); // number[]
需要注意两点:第一,有默认值的类型参数必须放在参数列表的末尾,就像函数的可选参数一样;第二,默认值只是“不传时的 fallback”,显式传入时仍然以传入的为准。
五、使用泛型时的几个提醒
第一,泛型不是 any 的替代品。写出尖括号里的 T 却不在任何地方使用它,或者上来就把所有参数写成泛型,都是没有意义的。类型参数应当真实参与到参数类型、返回值类型或成员类型里。
第二,类型参数的名字可以更表意。单字母 T、K、V、E 是社区约定俗成的做法,但在复杂场景下,用 Type、Key、Value 这样的完整单词会让代码更易读。
第三,不要过度设计。只在一个地方用到的简单逻辑,没必要为了“通用”硬套泛型;当第二、第三个相似类型出现时,再把它们抽象成泛型,往往是更合适的时机。
小结
泛型的学习路径可以概括为三步:先用类型参数把“类型当作参数”;再用 extends 约束为类型参数划定范围;最后用默认值减少重复书写。掌握这三步之后,像工具类型、条件类型这些进阶内容,都会建立在相同的思想之上。建议从自己项目里的重复函数入手,把它们改写成泛型版本,写几遍之后,泛型就不再是陌生的语法,而是日常工具箱里顺手的一件工具。
- 点赞
- 收藏
- 关注作者
评论(0)