为什么需要泛型
假设你要写一个函数,返回数组的第一个元素。如果没有泛型,你可能会这样写:
function first(arr: any[]): any {
return arr[0];
}
const num = first([1, 2, 3]); // 类型是 any,丢失了类型信息
const str = first(['a', 'b']); // 同样是 any
这段代码的问题在于,返回值的类型信息完全丢失了。调用者不知道 num 是 number 还是 string。泛型就是为解决这类问题而生的——它让你在定义函数、接口或类时,将类型作为参数传入,从而在保持类型安全的同时实现复用。
泛型函数
泛型函数的基本语法是在函数名后面加上尖括号 <T>:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // number | undefined
const str = first(['a', 'b']); // string | undefined
TypeScript 会自动推断 T 的类型,大多数情况下你不需要手动指定。但在一些复杂场景下,显式标注能让代码意图更清晰:
const result = first<number>([1, 2, 3]);
泛型接口与泛型类型别名
泛型不仅可以用在函数上,还可以用在接口和类型别名中:
// 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 泛型类型别名
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
使用泛型接口的经典场景是封装 API 请求:
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
泛型约束(extends)
有时候你希望泛型参数满足某些条件。比如,你写了一个函数,需要访问参数的 length 属性,但 T 可能是任何类型,不一定有 length。这时你可以用 extends 关键字添加约束:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength('hello'); // OK, string 有 length
logLength([1, 2, 3]); // OK, array 有 length
// logLength(123); // Error! number 没有 length
约束的另一个常见用途是确保泛型参数是某个对象的键:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
getProperty(user, 'name'); // OK, 返回 string
// getProperty(user, 'age'); // Error!
keyof 操作符
keyof 是泛型编程中极为重要的操作符。它获取一个类型的所有键,返回一个联合类型:
interface Person {
name: string;
age: number;
location: string;
}
type PersonKeys = keyof Person; // 'name' | 'age' | 'location'
keyof 经常和 extends 结合使用,用来约束泛型参数必须是对象的键。此外,它还是很多工具类型的基础。
条件类型(extends + infer)
条件类型是 TypeScript 类型系统中最强大的特性之一。它的语法类似三元表达式:
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
配合 infer 关键字,你可以在条件类型中"提取"部分类型信息:
// 提取数组元素类型
type ArrayItem<T> = T extends (infer U)[] ? U : never;
type Item1 = ArrayItem<string[]>; // string
type Item2 = ArrayItem<number[]>; // number
// 提取函数返回值类型
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type FnReturn = ReturnOf<Fn>; // string
常用工具类型
TypeScript 内置了一系列实用工具类型,它们本质上都是基于泛型和条件类型实现的。掌握它们能显著提升你的类型体操水平:
Partial<T> —— 全部变可选
interface Todo {
title: string;
description: string;
completed: boolean;
}
function updateTodo(id: number, fields: Partial<Todo>) {
// fields 的每个属性都是可选的
}
updateTodo(1, { completed: true }); // OK
Pick<T, K> —— 选取指定属性
type TodoPreview = Pick<Todo, 'title' | 'completed'>;
// { title: string; completed: boolean; }
Omit<T, K> —— 排除指定属性
type TodoWithoutDesc = Omit<Todo, 'description'>;
// { title: string; completed: boolean; }
Record<K, V> —— 构建键值对类型
type PageInfo = Record<'home' | 'about' | 'contact', { title: string; url: string }>;
// { home: { ... }; about: { ... }; contact: { ... }; }
其它常用工具类型速查
Required<T>—— 所有属性变为必选Readonly<T>—— 所有属性变为只读Exclude<T, U>—— 从联合类型中排除Extract<T, U>—— 从联合类型中提取NonNullable<T>—— 排除 null 和 undefinedReturnType<T>—— 获取函数返回值类型Parameters<T>—— 获取函数参数类型(元组)
泛型 React 组件实战
在 React 中,泛型最常见的应用场景是写可复用的列表、表单和选择器组件。以下是一个泛型 Select 组件的例子:
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select
value={getValue(value)}
onChange={(e) => {
const selected = options.find(
(opt) => getValue(opt) === e.target.value
);
if (selected) onChange(selected);
}}
>
{options.map((opt) => (
<option key={getValue(opt)} value={getValue(opt)}>
{getLabel(opt)}
</option>
))}
</select>
);
}
这个组件可以接受任意类型的选项——不管是简单的字符串数组,还是复杂的对象数组,都能提供完整的类型推导。
泛型的三个实用模式
模式一:泛型工厂函数
当你需要创建一系列结构相同但类型不同的对象时,泛型工厂函数非常有用:
function createList<T>(items: T[]): { items: T[]; add(item: T): void; remove(index: number): void } {
return {
items,
add(item: T) { this.items.push(item); },
remove(index: number) { this.items.splice(index, 1); },
};
}
const numberList = createList<number>([1, 2, 3]);
const stringList = createList<string>(['a', 'b']);
模式二:类型安全的 Event Emitter
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; ctrl: boolean };
focus: undefined;
};
class TypedEmitter<T extends Record<string, any>> {
private handlers = new Map<keyof T, Set<Function>>();
on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
emit<K extends keyof T>(event: K, data: T[K]) {
this.handlers.get(event)?.forEach((fn) => fn(data));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(x, y)); // 完全类型安全
模式三:Builder 模式
class QueryBuilder<T> {
private conditions: string[] = [];
where<K extends keyof T>(field: K, value: T[K]): this {
this.conditions.push(`${String(field)} = ${JSON.stringify(value)}`);
return this;
}
build(): string {
return `SELECT * WHERE ${this.conditions.join(' AND ')}`;
}
}
interface Product { id: number; name: string; price: number; }
const sql = new QueryBuilder<Product>()
.where('price', 99)
.where('name', 'Widget')
.build();
总结
泛型是 TypeScript 类型系统的基石。从简单的泛型函数开始,到泛型接口、泛型约束,再到条件类型和工具类型——每一步都在让你的代码变得更加类型安全的同时,保持高度的可复用性。建议从日常开发中最常见的场景开始练习:用 Partial 做更新接口、用 Pick/Omit 做 DTO 类型、用泛型封装 API 请求。当这些变成肌肉记忆之后,再逐步深入到条件类型和 infer 的高级用法。