泛型的本质:类型级别的函数
从值空间到类型空间的映射
泛型参数 <T> 本质上是一个类型级别的函数参数——它让类型定义可以接受输入类型,产生输出类型。理解这个类比,是掌握复杂泛型(条件类型、infer)的基础。
类型参数(Type Parameter)
尖括号中的大写字母(T、K、V、R 等),是类型的占位符。调用函数或实例化类时,TypeScript 通过类型推断或显式指定来确定具体类型。约定:T 表示通用类型,K 表示键类型,V 表示值类型,R 表示返回类型,E 表示错误类型。
类型推断(Type Inference)
大多数情况下,TypeScript 可以从函数参数值自动推断出类型参数,无需手动指定。如
identity(42),TypeScript 看到参数是 number,自动推断 T = number。只有在推断失败时(如泛型工厂函数无参数),才需要显式 identity<number>。协变与逆变(Covariance / Contravariance)
泛型在不同位置的方差不同:函数返回值位置的类型参数是协变的(子类可以赋给父类位置);函数参数位置是逆变的(父类可以赋给子类位置)。TypeScript 4.7 引入了
out T(协变标记)和 in T(逆变标记)来显式声明方差。const 类型参数(TypeScript 5.0+)
在泛型函数的类型参数前加
const,让 TypeScript 在推断时保留字面量类型(类似 as const)。如 function pick<const T extends object, ...>,调用时数组/对象参数会保留精确的字面量类型,而不是被拓宽为 string/number 等宽类型。// TypeScript 5.0+ const 类型参数
function makeArray<const T>(...args: T[]): T[] {
return args;
}
// 不加 const:类型被拓宽
function makeArrayWide<T>(...args: T[]): T[] { return args; }
const a = makeArrayWide('left', 'right'); // T = string(拓宽)
// 加 const:保留字面量类型
const b = makeArray('left', 'right'); // T = 'left' | 'right'(保留字面量)
// b 的类型是 ('left' | 'right')[],不是 string[]
// 实际应用:路由类型安全
function createRoutes<const T extends string>(...routes: T[]): Set<T> {
return new Set(routes);
}
const routes = createRoutes('/home', '/about', '/profile');
// routes 类型是 Set<'/home' | '/about' | '/profile'>
泛型基础
为什么需要泛型?
假设你想写一个通用的「栈」(Stack)数据结构,如果不用泛型,你必须为每种类型写一个专用版本(StringStack、NumberStack…),或者用 any 失去类型安全。泛型解决了这个问题——在保持类型安全的同时,让代码具有最大的复用性。
// 泛型函数:最简单的例子
// T 是类型参数,调用时确定具体类型
function identity<T>(arg: T): T {
return arg;
}
// 显式指定类型参数
identity<string>('hello'); // 返回 string
// 类型推断:TypeScript 从参数自动推导 T
identity(42); // T 推断为 number
identity([1, 2, 3]); // T 推断为 number[]
泛型栈实现
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.at(-1); }
get size(): number { return this.items.length; }
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push('2'); // ❌ 编译错误:Argument of type 'string' is not assignable
const strStack = new Stack<string>();
strStack.push('hello'); // ✅
泛型约束
// 约束:T 必须有 length 属性
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength('hello'); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ 数组有 length
logLength(42); // ❌ number 没有 length
// keyof 约束:确保 key 是对象的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: '张三', age: 28, email: 'a@b.com' };
getProperty(user, 'name'); // ✅ 返回 string
getProperty(user, 'age'); // ✅ 返回 number
getProperty(user, 'phone'); // ❌ 'phone' 不是 User 的键
内置工具类型
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PartialUser = Partial<User>; // 所有属性变可选
type RequiredUser = Required<User>; // 所有属性变必填
type ReadonlyUser = Readonly<User>; // 所有属性变只读
type PublicUser = Pick<User, 'id' | 'name' | 'email'>; // 只取部分
type SafeUser = Omit<User, 'password'>; // 去掉密码字段
// Record:创建键值类型的对象
type RoleMap = Record<'admin' | 'user' | 'guest', string[]>;
// 函数相关工具类型
type FetchFn = (url: string, options?: RequestInit) => Promise<Response>;
type FetchParams = Parameters<FetchFn>; // [string, RequestInit?]
type FetchReturn = ReturnType<FetchFn>; // Promise<Response>
泛型的实战场景
泛型在实际项目中无处不在:React 的 useState<T>、Promise<T>、Array<T>、事件处理器类型、API 响应类型、数据库查询结果类型……理解泛型是掌握 TypeScript 高级用法的基石。
泛型高级用法
多类型参数
// 多个类型参数的函数
function zip<A, B>(arr1: A[], arr2: B[]): [A, B][] {
return arr1.map((item, i) => [item, arr2[i]]);
}
zip([1, 2, 3], ['a', 'b', 'c']); // [number, string][]
// 泛型接口:描述通用 API 响应格式
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// 每个端点返回具体的类型
type UserResponse = ApiResponse<User>;
type ProductListResponse = ApiResponse<Product[]>;
// 泛型工厂函数:创建带类型的 Repository 对象
function createRepository<T extends { id: number }>(data: T[]) {
return {
findById(id: number): T | undefined {
return data.find(item => item.id === id);
},
findAll(): T[] {
return [...data];
},
save(item: T): void {
const index = data.findIndex(i => i.id === item.id);
if (index >= 0) data[index] = item;
else data.push(item);
}
};
}
const userRepo = createRepository<User>([]);
userRepo.findById(1); // 返回 User | undefined
默认类型参数
// TypeScript 2.3+ 支持泛型默认值
interface Container<T = string> {
value: T;
label: string;
}
// 不提供类型参数时,T 默认为 string
const box: Container = { value: 'hello', label: 'greeting' };
const numBox: Container<number> = { value: 42, label: 'answer' };
// React 组件中的应用(React.FC 默认 Props = {})
type FC<P = {}> = (props: P) => JSX.Element | null;
泛型约束进阶
条件约束与类型推导结合
// 多约束:K 必须同时满足多个条件
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b }; // 返回类型精确是 T & U,不是 object
}
const merged = merge({ name: '张三' }, { age: 28 });
merged.name; // ✅ string
merged.age; // ✅ number
// 约束链:一个类型参数约束另一个
function getValue<T, K extends keyof T = keyof T>(obj: T, key?: K):
K extends keyof T ? T[K] : T[keyof T] {
return key ? obj[key] : Object.values(obj)[0] as any;
}
// 条件约束:根据类型参数的性质改变返回类型
type Flatten<T> = T extends (infer Item)[] ? Item : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number(不是数组,原样返回)
type C = Flatten<boolean[]>; // boolean
泛型常见误区:过度使用 any 约束
以下写法虽然语法合法,但失去了泛型的意义:function fn<T extends any>(x: T): T——这和 function fn(x: any): any 几乎等价,类型检查形同虚设。约束应精确表达实际需求,如 T extends object(任何对象)或 T extends { id: number }(有 id 属性的对象),而不是 T extends any。
工具类型的手动实现
理解工具类型的实现原理有助于创建自定义工具类型:
// Partial:所有属性变可选
type Partial<T> = { [K in keyof T]?: T[K] };
// Required:所有属性变必填(去掉 ?)
type Required<T> = { [K in keyof T]-?: T[K] };
// Readonly:所有属性变只读
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Pick:从对象类型提取部分属性
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit:从对象类型排除部分属性
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Exclude:从联合类型中排除
type Exclude<T, U> = T extends U ? never : T;
type R = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
本章小结
本章核心要点
- 泛型的本质:用类型参数 T 延迟类型绑定,调用时确定具体类型;既保持类型安全,又实现代码复用。
- 泛型约束 extends:
T extends { length: number }限制 T 必须有 length 属性;K extends keyof T确保 K 是 T 的键。 - 内置工具类型:Partial/Required/Readonly/Pick/Omit/Record/ReturnType/Parameters 是最常用的工具类型,都基于映射类型和条件类型实现。
- 泛型默认值:
<T = string>设置类型参数默认值,不指定时使用默认类型,减少冗余的类型参数声明。 - 实战应用:API 响应类型 ApiResponse<T>、通用 Repository、React useState<T> 都是泛型的典型应用场景。