TypeScript 泛型是类型系统中最强大的特性之一。它允许你通过参数化类型来编写灵活、可复用且类型安全的代码。泛型让你无需为每种数据类型重复编写函数或类,只需编写一次即可与任何类型一起使用,同时保持完整的类型安全。本指南涵盖从基础到高级模式的所有内容。
你的第一个泛型函数
经典的起点是身份函数——一个返回其接收内容的函数。如果不使用泛型,你要么使用 any 丢失类型信息,要么为每种类型编写单独的函数。
使用泛型,你定义一个类型参数(通常命名为 T),作为占位符。调用函数时,TypeScript 会自动推断实际类型。
// Without generics — loses type info
function identityAny(value: any): any {
return value;
}
const result1 = identityAny("hello"); // type: any (not string!)
// With generics — preserves type info
function identity<T>(value: T): T {
return value;
}
const result2 = identity("hello"); // type: string ✓
const result3 = identity(42); // type: number ✓
const result4 = identity(true); // type: boolean ✓
// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: string泛型函数
泛型函数可以接受多个类型参数,甚至可以有默认类型。这使它们在构建工具函数时非常灵活。
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // type: [string, number]
// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]
// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, "x"); // T inferred as string
const numbers = createArray(3, 0); // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to string默认类型参数的工作方式与默认函数参数相同——如果调用者未指定类型,则使用默认类型。
泛型接口和类型
泛型不仅限于函数。你可以参数化接口和类型别名,创建适用于任何类型的灵活数据结构。
// Generic interface
interface Box<T> {
value: T;
label: string;
}
const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };
// Generic interface with multiple params
interface KeyValuePair<K, V> {
key: K;
value: V;
}
// Generic type alias
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };
// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;
// Generic type for nullable values
type Nullable<T> = T | null | undefined;泛型接口在定义 API 响应、集合和数据容器的结构时特别有用。
泛型类
类也可以是泛型的。这对于构建栈、队列、链表等数据结构或任何需要与多种类型一起工作的容器非常有用。
// Generic Stack class
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[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined
// Generic class with multiple type params
class HashMap<K, V> {
private map = new Map<K, V>();
set(key: K, value: V): void {
this.map.set(key, value);
}
get(key: K): V | undefined {
return this.map.get(key);
}
has(key: K): boolean {
return this.map.has(key);
}
}类型参数在整个类中都可用——在属性、方法参数和返回类型中。
使用 extends 的泛型约束
不受约束的泛型接受任何类型,但有时你需要缩小允许的类型范围。extends 关键字让你可以添加约束。
使用 keyof 约束对象键
一个非常常见的模式是将类型参数约束为另一个类型的有效键。这确保了类型安全的属性访问。
// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name"); // type: string ✓
getProperty(user, "age"); // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"
// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => { result[key] = obj[key]; });
return result;
}
const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }约束到特定结构
你可以通过扩展接口或对象类型来要求类型参数具有某些属性。
// Constraint to objects with a length property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(`Length: ${value.length}`);
return value;
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42); // Error — number has no length property
// Constraint to objects with an id
interface HasId {
id: number | string;
}
function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
const found = findById(users, 1);
// type: { id: number; name: string } | undefined条件类型
条件类型允许你根据条件在两种类型之间选择,使用 T extends U ? X : Y 语法。它们是许多高级类型计算的基础。
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"hello">; // true
// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type D = Flatten<string[]>; // string
type E = Flatten<number[][]>; // number[]
type F = Flatten<string>; // string (not an array, returned as-is)
// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type G = MyReturnType<() => string>; // string
type H = MyReturnType<(x: number) => boolean>; // boolean
// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>; // number条件类型与 infer 结合时变得特别强大,后者允许你从其他类型内部提取类型。
映射类型
映射类型允许你转换现有类型的属性来创建新类型。它们使用 [K in keyof T] 语法遍历键。
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required (like Required<T>)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
interface User {
name: string;
age: number;
active: boolean;
}
type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }
// Mapped type with conditional
type OptionalNullable<T> = {
[K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};你可以在映射类型中添加或删除 readonly 和 ?(可选)等修饰符。
内置工具类型
TypeScript 附带了许多使用泛型构建的工具类型。以下是最常用的:
| 工具类型 | 描述 | 示例 |
|---|---|---|
Partial<T> | 使所有属性可选 | Partial<User> |
Required<T> | 使所有属性必需 | Required<Partial<User>> |
Pick<T, K> | 选取指定属性 | Pick<User, "name" | "email"> |
Omit<T, K> | 排除指定属性 | Omit<User, "password"> |
Record<K, V> | 创建键值类型映射 | Record<string, number> |
Readonly<T> | 使所有属性只读 | Readonly<Config> |
ReturnType<T> | 提取函数返回类型 | ReturnType<typeof fn> |
Parameters<T> | 提取函数参数类型元组 | Parameters<typeof fn> |
// Utility types in action
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// For update forms — all fields optional
type UpdateUser = Partial<User>;
// Public user — omit password
type PublicUser = Omit<User, "password">;
// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;
// Lookup table
type UserLookup = Record<number, User>;
// Immutable user from API
type FrozenUser = Readonly<User>;
// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>; // Promise<User>
type FetchParams = Parameters<typeof fetchUser>; // [number]真实世界的模式
泛型 API 响应包装器
泛型最常见的用途之一是用一致的结构包装 API 响应。这确保每个端点都以相同的格式返回数据,同时保留特定的数据类型。
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
}
// Usage with specific types
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}
// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓
const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓泛型 React 组件 Props
泛型让你构建适用于不同数据类型的 React 组件,同时保持 props 的类型安全。
// Generic list component props
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
onItemClick?: (item: T) => void;
emptyMessage?: string;
}
function GenericList<T>({
items,
renderItem,
keyExtractor,
onItemClick,
emptyMessage = "No items",
}: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, index) => (
<li
key={keyExtractor(item)}
onClick={() => onItemClick?.(item)}
>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage — TypeScript infers T from items
<GenericList
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
onItemClick={(user) => console.log(user.email)}
/>泛型仓储模式
仓储模式将数据访问抽象在泛型接口背后。这允许你在不更改业务逻辑的情况下切换数据源。
// Generic repository interface
interface Entity {
id: number | string;
}
interface Repository<T extends Entity> {
findAll(): Promise<T[]>;
findById(id: T["id"]): Promise<T | null>;
create(data: Omit<T, "id">): Promise<T>;
update(id: T["id"], data: Partial<T>): Promise<T>;
delete(id: T["id"]): Promise<boolean>;
}
// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
constructor(private baseUrl: string) {}
async findAll(): Promise<T[]> {
const res = await fetch(this.baseUrl);
return res.json();
}
async findById(id: T["id"]): Promise<T | null> {
const res = await fetch(`${this.baseUrl}/${id}`);
if (!res.ok) return null;
return res.json();
}
async create(data: Omit<T, "id">): Promise<T> {
const res = await fetch(this.baseUrl, {
method: "POST",
body: JSON.stringify(data),
});
return res.json();
}
async update(id: T["id"], data: Partial<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
});
return res.json();
}
async delete(id: T["id"]): Promise<boolean> {
const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
return res.ok;
}
}
// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]泛型事件发射器
类型安全的事件发射器确保事件名称及其负载被正确类型化。不再有未知数据的字符串事件。
// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;
class TypedEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: EventCallback<Events[K]>[];
} = {};
on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
const cbs = this.listeners[event];
if (cbs) {
this.listeners[event] = cbs.filter(cb => cb !== callback);
}
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const cbs = this.listeners[event];
if (cbs) {
cbs.forEach(cb => cb(payload));
}
}
}
// Define your event types
interface AppEvents {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"item:added": { itemId: string; quantity: number };
"error": { message: string; code: number };
}
const emitter = new TypedEmitter<AppEvents>();
// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
console.log(payload.userId); // type: string ✓
console.log(payload.timestamp); // type: number ✓
});
// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});常见错误
以下是开发者在使用泛型时最常犯的错误及其修正:
| 错误 | 问题 | 修正 |
|---|---|---|
| 使用 any 代替泛型 | 丢失所有类型信息 | 使用类型参数 T 代替 any |
| 过度约束类型参数 | 使泛型对有效类型不可用 | 只约束你实际需要的部分 |
| 不必要的类型参数 | 添加只使用一次的 T 增加了复杂性 | 如果 T 只出现在返回类型中,直接使用具体类型 |
| 访问属性时不使用约束 | TypeScript 错误:类型 T 上不存在属性 | 添加 extends 约束:T extends { prop: type } |
| 混淆泛型默认值和约束 | 默认类型不限制——调用者仍可传递其他类型 | 用 extends 限制,用 = 设置默认值 |
| 没有在可能时进行推断 | 在 TS 可以推断时显式传递类型 | 尽可能让 TypeScript 从参数推断类型 |
| 使用 T[] 而不是 readonly T[] | 允许修改应为只读的数组 | 对来自 API 的不可变数据使用 readonly T[] |
// ❌ Mistake: Using any
function badParse(json: string): any {
return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!
// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
try {
const parsed = JSON.parse(json);
return validator(parsed) ? parsed : null;
} catch {
return null;
}
}
// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
return arr.length; // T is never meaningfully used
}
// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
return arr.length;
}
// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
return obj.name; // Error: Property 'name' does not exist on type 'T'
}
// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
return obj.name; // OK ✓
}常见问题
泛型和 any 类型有什么区别?
泛型在整个函数或类中保留类型信息,而 any 会丢弃它。使用泛型,如果传入字符串,TypeScript 知道返回类型也是字符串。使用 any,所有类型信息都会丢失。
什么时候应该使用泛型而不是函数重载?
当逻辑对所有类型相同且你只需要保留类型关系时使用泛型。当实现因输入类型而异,或需要将特定输入类型映射到特定输出类型时使用重载。
在 TSX 文件中可以将泛型用于箭头函数吗?
可以,但需要添加尾随逗号以避免 JSX 歧义:const fn = <T,>(arg: T): T => arg。逗号告诉解析器这是泛型,不是 JSX 标签。
泛型应该有多少个类型参数?
越少越好。一到两个很常见。三个有时是必要的。如果需要三个以上,考虑重构——你的抽象可能过于复杂。每个类型参数都应有明确的用途。
泛型会影响运行时性能吗?
不会。泛型纯粹是编译时特性。它们在编译过程中被完全擦除,不会产生运行时开销。生成的 JavaScript 中没有泛型类型参数的痕迹。
类型参数的命名约定是什么?
单个大写字母是惯例:T 表示类型,K 表示键,V 表示值,E 表示元素,R 表示返回类型。对于有多个参数的复杂泛型,TData、TError 等描述性名称也可以接受,并能提高可读性。