TypeScript 泛型
泛型(generic)是 TS 里"高级但必修"的概念。听起来唬人,其实核心就一句话:把类型也当成参数。就像函数用形参接收不同的值,泛型用类型参数(通常叫 T)接收不同的类型,让你写出"对任何类型都通用"的代码。这一章把它讲透。
1. 为什么需要泛型?
先看一个问题:写一个"取数组第一个元素"的函数。如果不用泛型,只能用 any,等于白写 TS:
// 问题:写一个"取数组第一个元素"的函数
// 如果不用泛型,只能用 any(失去类型检查)
function firstAny(arr: any[]): any {
return arr[0];
}
const n = firstAny([1, 2, 3]); // n 是 any,没有类型保护
// 用泛型:<T> 是"类型参数",调用时再确定具体类型
function first<T>(arr: T[]): T {
return arr[0];
}
const a = first([1, 2, 3]); // T 推断为 number,a 是 number
const b = first(["a", "b"]); // T 推断为 string,b 是 string
const c = first([true, false]); // T 推断为 boolean,c 是 boolean关键看 <T> 这个尖括号——它声明了一个类型参数 T。调用 first([1,2,3]) 时,TS 看到参数是 number 数组,就把 T 替换成 number,于是返回值 a 就是 number,有了完整的类型保护。
2. 理解 T:类型的占位符
把 T 想象成"占位符":写函数时不固定具体类型,调用时再确定。T 只是约定俗成的名字,可以叫任何名字:
// 把 T 想象成"占位符":写函数时不固定具体类型
// 调用时,TS 根据你传入的参数,自动推断 T 是什么
function identity<T>(value: T): T {
return value;
}
// 也可以显式指定 T(通常让 TS 推断即可)
identity<string>("hello"); // 显式指定 T = string
identity(42); // 推断 T = number
// T 只是约定俗成的名字,可以叫任何名字
// 常见: T(Type)、K(Key)、V(Value)、E(Element)、R(Return)
function mapValue<K, V>(key: K, value: V): V {
return value;
}3. 多个类型参数
泛型不只能有一个参数,可以同时有多个(比如键值对场景):
// 多个类型参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p = pair("name", 20); // [string, number]4. 泛型接口(重点)
泛型接口是实战中最常见的形态——尤其是API 响应、通用容器、状态管理。掌握它,你就能看懂大部分开源库的类型定义:
// 泛型接口:常用于"通用容器""通用仓库"
interface Box<T> {
value: T;
label: string;
}
const box1: Box<number> = { value: 42, label: "数字盒" };
const box2: Box<string> = { value: "hi", label: "字符串盒" };
// 实战:分页响应是经典的泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
const userRes: ApiResponse<{ name: string }> = {
code: 200,
message: "ok",
data: { name: "小明" },
};ApiResponse<T> 这个模式在前端项目里几乎天天用:后端返回的数据结构是固定的(code、message、data),但 data 的具体形状因接口而异——用泛型 T 表示 data 的类型,完美。
5. 泛型类
类也可以有类型参数,常用于通用数据结构(栈、队列、集合):
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const top = numStack.pop(); // number | undefined
const strStack = new Stack<string>();
strStack.push("a");6. 泛型约束:extends
默认情况下,T 可以是任何类型。但有时你需要限制它必须满足某些条件——比如"必须有 length 属性"。这时用 extends 做约束:
// 泛型约束:用 extends 限制 T 必须满足某些条件
interface HasLength {
length: number;
}
// T extends HasLength 表示:T 至少要有 length 属性
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ 数组有 length
// logLength(42); // ❌ number 没有 length
// keyof 约束:K 必须是 T 的某个键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "小明", age: 20 };
getProperty(user, "name"); // string
getProperty(user, "age"); // number
// getProperty(user, "xxx"); // ❌ "xxx" 不是 user 的键其中 keyof T(T 的所有键的联合)是非常高频的搭配,用于"安全访问对象属性"。上面 getProperty 函数:传不存在的键会直接报错,避免了运行时的 undefined bug。
7. 默认类型参数
泛型参数可以有默认值,不指定时使用默认:
// 默认类型参数:不指定时用默认值
interface Paginated<T = string> {
items: T[];
}
const p1: Paginated = { items: ["a", "b"] }; // T 默认 string
const p2: Paginated<number> = { items: [1, 2] }; // T 显式 number小结
泛型的本质:把类型当参数。掌握三个要点——<T> 声明类型参数、T[]/Box<T> 使用类型参数、extends 给类型参数加约束。新手先学会读泛型、会用简单泛型函数/接口;看到框架里复杂的 <T extends ...> 再慢慢消化,不用一上来就全懂。
← 上一篇 TypeScript 函数
下一篇 TypeScript 联合与交叉类型 →