TypeScript 泛型:让代码更灵活、更安全
在开发中,我们常常会遇到这样的情景:写一个函数,它能处理多种类型的数据,比如数组、字符串、数字,甚至自定义对象。如果用普通类型声明,就必须为每种类型写一遍逻辑,这不仅重复,还容易出错。TypeScript 泛型正是为了解决这类“类型复用”问题而生的。
你可以把泛型想象成一个“模板”,它不指定具体类型,而是用一个占位符(比如 T)来代表未来可能传入的类型。这样,同一个函数或类,就能在不修改代码的前提下,安全地处理不同类型的值。
这篇文章会带你一步步理解 TypeScript 泛型的核心概念,从基础语法到高级用法,通过真实案例帮你掌握它的实用技巧。
什么是 TypeScript 泛型?简单说就是“类型参数”
TypeScript 泛型允许我们在定义函数、接口、类时,不指定具体的类型,而是用一个类型参数来占位。这个参数在实际使用时会被替换为真实的类型。
举个例子,我们想写一个函数,它能返回传入的任意值。如果不用泛型,只能写成:
function identity(arg: any): any {
return arg;
}
这个写法虽然能工作,但失去了类型推断的能力。比如:
const result = identity("Hello");
console.log(result.toUpperCase()); // 没有类型提示!
因为返回值是 any,TypeScript 不知道 result 是字符串,所以无法提供智能提示。
而使用泛型后,代码就变得既灵活又安全:
function identity<T>(arg: T): T {
return arg;
}
// 使用时指定类型
const strResult = identity<string>("Hello");
const numResult = identity<number>(123);
console.log(strResult.toUpperCase()); // ✅ 有类型提示
console.log(numResult.toFixed(2)); // ✅ 有类型提示
注释:
<T>表示这是一个泛型参数,T 可以是任意名称,常见用T、K、V等。identity<T>表示这个函数接受一个类型为 T 的参数,也返回 T 类型的值。
泛型函数:让函数支持多种类型
泛型函数的核心思想是“类型延迟绑定”——类型在调用时才确定。这使得我们能写出更通用、可复用的函数。
创建一个通用的打印函数
假设我们想写一个函数,用来打印数组内容,但不关心数组里是什么类型:
function printArray<T>(arr: T[]): void {
arr.forEach(item => {
console.log(item);
});
}
// 使用示例
printArray<string>(["a", "b", "c"]);
printArray<number>([1, 2, 3]);
printArray<boolean>([true, false, true]);
注释:
T[]表示“任意类型 T 的数组”。printArray<T>的泛型参数 T 被用来约束数组元素的类型。调用时传入<string>,TypeScript 就知道数组里是字符串。
类型推断让调用更简洁
TypeScript 支持类型推断,如果传入的参数类型明确,就不需要手动写泛型参数:
printArray(["x", "y", "z"]); // ✅ 自动推断为 string[]
printArray([10, 20, 30]); // ✅ 自动推断为 number[]
这大大减少了代码冗余,让代码更简洁。
泛型接口:定义灵活的结构
接口是 TypeScript 中定义对象结构的重要工具。当接口需要支持多种类型时,泛型就派上用场了。
定义一个通用的响应接口
在前后端交互中,我们常看到类似结构:
interface ApiResponse<T> {
code: number;
message: string;
data: T; // 数据部分类型不确定
}
注释:
<T>表示这个接口的data字段可以是任意类型。这样,无论返回的是用户列表、订单详情,还是单个对象,都能安全使用。
使用示例:
const userResponse: ApiResponse<User> = {
code: 200,
message: "success",
data: { id: 1, name: "Alice" }
};
const listResponse: ApiResponse<User[]> = {
code: 200,
message: "success",
data: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]
};
注释:
ApiResponse<User>表示data是User类型;ApiResponse<User[]>表示data是User数组。TypeScript 会严格检查类型,避免错误。
泛型类:让类支持多种数据类型
类是面向对象编程的核心。当一个类需要处理多种数据类型时,泛型可以让类更加通用。
实现一个通用的栈(Stack)结构
栈是一种后进先出(LIFO)的数据结构。我们可以用泛型实现一个支持任意类型的栈:
class Stack<T> {
private items: T[] = [];
// 入栈
push(item: T): void {
this.items.push(item);
}
// 出栈
pop(): T | undefined {
return this.items.pop();
}
// 查看栈顶元素
peek(): T | undefined {
return this.items.length > 0 ? this.items[this.items.length - 1] : undefined;
}
// 检查是否为空
isEmpty(): boolean {
return this.items.length === 0;
}
// 获取大小
size(): number {
return this.items.length;
}
}
// 使用示例
const stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.pop()); // "b"
const numberStack = new Stack<number>();
numberStack.push(100);
numberStack.push(200);
console.log(numberStack.peek()); // 200
注释:
Stack<T>中的 T 代表栈中元素的类型。创建实例时传入string或number,TypeScript 会自动绑定类型,确保操作安全。
泛型约束:限制可接受的类型
有时候我们希望泛型只能接受特定类型的值。比如,我们想让泛型函数只能处理有 .length 属性的类型(如字符串、数组)。
这时可以使用“泛型约束”:
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 使用示例
console.log(getLength("hello")); // ✅ 3
console.log(getLength([1, 2, 3])); // ✅ 3
console.log(getLength(123)); // ❌ 编译错误!number 没有 length 属性
注释:
T extends { length: number }表示 T 必须是一个拥有length属性且类型为number的对象。这限制了泛型只能用于支持length的类型。
常见约束类型
| 类型约束 | 说明 |
|---|---|
T extends string |
只能是字符串类型 |
T extends number |
只能是数字类型 |
T extends object |
只能是对象类型 |
T extends { name: string } |
必须有 name 字段且为字符串 |
泛型高级技巧:联合类型与映射
使用泛型处理对象属性
有时我们需要从一个对象中提取某些字段,比如从 User 类型中提取 name 和 age:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
}
// 提取 name 和 age
type UserBasic = Pick<User, "name" | "age">;
const userBasic: UserBasic = {
name: "Alice",
age: 25
};
注释:
keyof T获取 T 的所有键名(即属性名);P in K是映射类型,表示遍历 K 中的每个键。Pick是 TypeScript 内置工具类型,用于提取对象的部分属性。
总结:泛型是 TypeScript 的“万能钥匙”
TypeScript 泛型不是一时兴起的语法糖,而是让代码更安全、更可复用的核心机制。它让开发者在不牺牲类型安全的前提下,写出高度通用的函数、接口和类。
从一个简单的 identity 函数,到复杂的 Pick 工具类型,泛型贯穿了整个 TypeScript 的类型系统。掌握它,意味着你真正进入了“类型编程”的世界。
无论你是初学者还是中级开发者,只要你在项目中遇到“我要处理多种类型但不想写重复代码”的场景,就该想到 TypeScript 泛型。
记住:泛型不是为了炫技,而是为了写出更健壮、更易维护的代码。
继续练习,多写几个泛型函数,多定义几个泛型接口,你很快就会发现,它已经成了你日常开发中不可或缺的一部分。