TypeScript 泛型终极指南:从懵圈到精通
前言
如果你学 TypeScript 时被泛型搞得头晕目眩,恭喜你,你不是一个人。泛型(Generics)可能是 TypeScript 中最让初学者困惑的概念之一。那些满屏的 <T>、<K, V>、extends 看起来就像天书。
但其实,泛型的核心思想出奇地简单。读完这篇文章,你会发现泛型不过是"类型层面的函数"而已。
一、为什么需要泛型?
问题:重复的代码
假设你要写一个函数,返回数组的第一个元素:
function getFirstNumber(arr: number[]): number {
return arr[0];
}
getFirstNumber([1, 2, 3]); // 可以用
但如果你有字符串数组呢?你得再写一个:
function getFirstString(arr: string[]): string {
return arr[0];
}
对象数组?再写一个:
function getFirstObject(arr: object[]): object {
return arr[0];
}
这也太蠢了!函数逻辑完全一样,唯一的区别只是类型不同。
解决方案:泛型
function getFirst<T>(arr: T[]): T {
return arr[0];
}
// 一个函数,搞定所有类型
getFirst<number>([1, 2, 3]); // 返回 number
getFirst<string>(['a', 'b', 'c']); // 返回 string
getFirst([true, false]); // TypeScript 自动推断为 boolean
泛型让你写一次代码,适配所有类型。
二、泛型的本质:类型层面的函数
这是理解泛型的关键洞察:
// 普通函数:接收值,返回值
function double(x: number): number {
return x * 2;
}
// 泛型:接收类型,返回类型
type ArrayOf<T> = T[];
让我们对比一下:
| 概念 | 普通函数 | 泛型 |
|---|---|---|
| 输入 | 值(如 5) |
类型(如 number) |
| 输出 | 值(如 10) |
类型(如 number[]) |
| 参数 | (x: number) |
<T> |
| 调用 | double(5) |
ArrayOf<number> |
泛型就是生成类型的函数,泛型参数就是这个函数的变量。
三、基础语法详解
1. 函数泛型
// 语法:function 函数名<泛型参数>(参数: 类型): 返回类型
function identity<T>(value: T): T {
return value;
}
// 使用方式一:显式指定类型
identity<number>(42); // T = number
identity<string>("hello"); // T = string
// 使用方式二:类型推断(推荐)
identity(42); // TypeScript 自动推断 T = number
identity("hello"); // TypeScript 自动推断 T = string
2. 类型别名泛型
// 语法:type 类型名<泛型参数> = 类型定义
type Container<T> = {
value: T;
id: number;
}
// 使用
const numContainer: Container<number> = {
value: 42,
id: 1
};
const strContainer: Container<string> = {
value: "hello",
id: 2
};
3. 接口泛型
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 使用
const userResponse: ApiResponse<User> = {
code: 200,
message: "成功",
data: { id: 1, name: "张三" }
};
const listResponse: ApiResponse<Article[]> = {
code: 200,
message: "成功",
data: [
{ title: "文章1", content: "内容1" },
{ title: "文章2", content: "内容2" }
]
};
四、实战案例:由浅入深
案例 1:数组操作
// 交换数组前两个元素
function swap<T>(arr: T[]): T[] {
if (arr.length < 2) return arr;
return [arr[1], arr[0], ...arr.slice(2)];
}
swap([1, 2, 3]); // [2, 1, 3]
swap(['a', 'b', 'c']); // ['b', 'a', 'c']
swap([true, false, true]); // [false, true, true]
案例 2:多个泛型参数
// 创建键值对
function makePair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
makePair("age", 25); // ["age", 25]
makePair(1, "first"); // [1, "first"]
makePair("user", { id: 100 }); // ["user", {id: 100}]
这里有两个类型参数:K(Key 的缩写)和 V(Value 的缩写)。
案例 3:Promise 类型(你肯定见过)
// Promise<T> 就是内置的泛型类型
async function fetchUser(): Promise<User> {
const response = await fetch('/api/user');
return response.json(); // 返回 User 类型
}
async function fetchNumber(): Promise<number> {
return 42;
}
Promise<User> 表示:这个 Promise 解析(resolve)后会得到 User 类型的数据。
五、泛型约束:限制类型的范围
有时候,你需要在泛型函数内部使用类型的某些特性(如属性或方法)。这时就需要泛型约束。
问题场景
function getLength<T>(item: T): number {
return item.length; // ❌ 报错!T 可能没有 length 属性
}
TypeScript 不知道 T 是否有 length 属性,所以报错。
解决方案:使用 extends 约束
// 约束 T 必须有 length 属性
function getLength<T extends { length: number }>(item: T): number {
return item.length; // ✅ 安全!
}
getLength("hello"); // 5(字符串有 length)
getLength([1, 2, 3]); // 3(数组有 length)
getLength({ length: 10 }); // 10(对象有 length)
// getLength(123); // ❌ 报错!数字没有 length
约束的本质
泛型约束就像函数参数的类型检查,确保传入的类型满足特定要求。
// 比喻:榨汁机只能榨"有汁水"的东西
interface Juiceable {
squeeze(): string;
}
function makeJuice<T extends Juiceable>(fruit: T): string {
return fruit.squeeze(); // 因为约束了 T 必须有 squeeze 方法
}
class Orange implements Juiceable {
squeeze() { return "🍊 橙汁"; }
}
class Apple implements Juiceable {
squeeze() { return "🍎 苹果汁"; }
}
class Rock {
// 没有 squeeze 方法
}
makeJuice(new Orange()); // ✅ "🍊 橙汁"
makeJuice(new Apple()); // ✅ "🍎 苹果汁"
// makeJuice(new Rock()); // ❌ 报错!石头不能榨汁
六、常见的泛型模式
1. keyof 约束:限制为对象的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
name: "李四",
age: 30,
email: "li@example.com"
};
const name = getProperty(user, "name"); // 类型是 string
const age = getProperty(user, "age"); // 类型是 number
// getProperty(user, "xxx"); // ❌ 报错!"xxx" 不是 user 的键
分解理解:
K extends keyof T:K 必须是 T 的键之一keyof T:获取 T 的所有键组成的联合类型("name" | "age" | "email")T[K]:获取对象 T 中键 K 对应值的类型
2. 默认类型参数
// 类似函数的默认参数
type Response<T = unknown> = {
data: T;
status: number;
}
type UserResponse = Response<User>; // T = User
type DefaultResponse = Response; // T = unknown(使用默认值)
3. 泛型组合
type AddNull<T> = T | null;
type AddArray<T> = T[];
// 组合使用
type NullableArray<T> = AddArray<AddNull<T>>;
type Example = NullableArray<number>; // (number | null)[]
七、React 中的泛型(实战)
// 通用的列表组件
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<div>
{items.map((item, index) => (
<div key={index}>{renderItem(item)}</div>
))}
</div>
);
}
// 使用
interface User {
id: number;
name: string;
}
<List<User>
items={users}
renderItem={(user) => <span>{user.name}</span>}
/>
八、记忆诀窍
每次看到泛型时,心里默念这个公式:
type/function Name<参数 extends 约束 = 默认值> = 返回值/实现
// ↑ ↑ ↑ ↑
// 类型变量 限制条件 可选默认 类型定义
例如:
type MyArray<T extends object = {}> = T[];
// ↑ ↑ ↑ ↑
// 参数名 必须是对象 默认空对象 返回数组
九、常见命名约定
| 字母 | 含义 | 使用场景 |
|---|---|---|
T |
Type | 通用类型参数 |
K |
Key | 对象的键 |
V |
Value | 对象的值 |
E |
Element | 数组元素 |
P |
Props | React 组件属性 |
S |
State | React 组件状态 |
但记住:你可以用任何名字,比如 <数据类型>、<ItemType> 都可以。
十、总结
泛型的三个核心理解:
-
泛型 = 类型层面的函数
- 接收类型参数,返回新类型
- 就像函数接收值参数,返回新值
-
泛型参数 = 类型变量
<T>就是一个占位符- 调用时才确定具体类型
-
泛型约束 = 原材料要求
- 因为内部使用了类型的某些特性
- 所以必须限制传入类型满足这些要求
下次看到复杂的泛型代码,试着这样拆解:
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
心里这样翻译:
- 这是一个类型函数
- 接收两个类型参数:
T(对象类型)和K(键类型) K有约束:必须是T的键- 返回类型:
T[K](对象中该键对应值的类型)
现在,去征服那些曾经让你头晕的泛型代码吧!
练习建议:把文中的例子都自己敲一遍,改改参数试试会发生什么。泛型不是用来背的,是用来理解的。当你能流畅地读懂 Promise<T>、Array<T>、Record<K, V> 这些内置泛型时,你就真正掌握了。
