泛型基础与应用场景深度解析
深入解析TypeScript泛型函数、泛型约束与常用泛型工具类型的核心原理与实战应用
泛型基础与应用场景深度解析
一句话概括
泛型是 TypeScript 的类型参数化能力——让你写一份代码,适应多种类型,同时保持完整的类型安全。
背景
在实际开发中,我们经常遇到这样的问题:
- 一个函数需要同时支持
string和number参数 - 一个工具类需要处理不同类型的数据结构
- 一个 API 请求函数的返回值类型随接口变化
用 any 可以解决,但代价是完全丢失类型检查。用函数重载可以解决,但代码冗长且难维护。
泛型就是 TypeScript 给出的最优解:既保持灵活性,又保留类型安全。
面试中,泛型是区分”用过 TypeScript”和”理解 TypeScript”的关键考点:
- “什么是泛型?” —— 基础题
- “泛型约束怎么用?” —— 进阶题
- “手写一个泛型工具类型” —— 深度题
概念与定义
什么是泛型?
泛型(Generics)是指在定义函数、接口或类时,不预先指定具体类型,而在使用时再指定的机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 没有泛型:要么丢失类型,要么重复代码
function identityAny(value: any): any {
return value;
}
// 类型信息丢失:identityAny("hello") 返回 any
function identityString(value: string): string {
return value;
}
function identityNumber(value: number): number {
return value;
}
// 代码重复
// 有泛型:一份代码,多种类型,类型安全
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // 返回 string
identity<number>(42); // 返回 number
identity("world"); // 类型推断:T = string,返回 string
泛型的核心思想
类型参数化:把类型当成参数,从调用者传入,而不是在定义时写死。
1
2
3
4
5
6
7
8
// T 就是类型参数,类似于函数参数
function genericFunc<T>(arg: T): T {
return arg;
}
// 调用时传入类型参数(类似于函数调用传参)
genericFunc<string>("hello");
genericFunc<number>(123);
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 最简单的泛型函数
function echo<T>(value: T): T {
return value;
}
echo("hello"); // string
echo(42); // number
echo(true); // boolean
// 泛型与数组
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first(["a", "b"]); // string | undefined
first([1, 2, 3]); // number | undefined
first([]); // undefined
// 泛型与箭头函数
const last = <T>(arr: T[]): T | undefined => arr[arr.length - 1];
// 注意:在 .tsx 文件中,<T> 可能被误认为 JSX 标签
// 解决方案:使用 <T,> 或 <T extends unknown>
const lastTsx = <T,>(arr: T[]): T | undefined => arr[arr.length - 1];
核心知识点拆解
1. 泛型函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 多个类型参数
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b } as T & U;
}
const result = merge({ name: "Alice" }, { age: 30 });
// result 的类型: { name: string } & { age: number }
// 泛型与默认类型
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
createArray(3, "x"); // string[]
createArray(3, 1); // number[](类型推断覆盖默认值)
createArray(3); // string[](使用默认类型)
2. 泛型接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 接口泛型
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 使用时指定具体类型
interface User {
id: number;
name: string;
}
const userResponse: ApiResponse<User> = {
code: 200,
message: "success",
data: { id: 1, name: "Alice" }
};
const listResponse: ApiResponse<User[]> = {
code: 200,
message: "success",
data: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
};
// 接口泛型的默认类型
interface PaginatedResponse<T = any> {
list: T[];
total: number;
page: number;
pageSize: number;
}
3. 泛型类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 泛型类:数据容器
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
get(index: number): T | undefined {
return this.items[index];
}
getAll(): T[] {
return [...this.items];
}
count(): number {
return this.items.length;
}
}
// 使用
const stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");
console.log(stringStore.get(0)); // "hello"
const numberStore = new DataStore<number>();
numberStore.add(42);
numberStore.add(100);
console.log(numberStore.getAll()); // [42, 100]
4. 泛型约束(extends)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 问题:T 太宽泛,无法访问任何属性
function logLength<T>(value: T): void {
// console.log(value.length); // ❌ 报错:T 不一定有 length
}
// 解决:用 extends 约束 T 必须有 length 属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(value.length); // ✅ 现在 T 一定有 length
}
logLength("hello"); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ array 有 length
logLength({ length: 10 }); // ✅ 对象有 length
// logLength(123); // ❌ number 没有 length
// 约束为对象类型
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 30 };
getProperty(person, "name"); // string
getProperty(person, "age"); // number
// getProperty(person, "email"); // ❌ "email" 不是 keyof person
5. 常用泛型工具类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Partial<T>:将所有属性变为可选
interface Todo {
title: string;
description: string;
completed: boolean;
}
type PartialTodo = Partial<Todo>;
// { title?: string; description?: string; completed?: boolean; }
// Pick<T, K>:从 T 中选取部分属性
type TodoPreview = Pick<Todo, "title" | "completed">;
// { title: string; completed: boolean; }
// Omit<T, K>:从 T 中排除部分属性
type TodoInfo = Omit<Todo, "completed">;
// { title: string; description: string; }
// Record<K, V>:构建键值对类型
type PageInfo = Record<"home" | "about" | "contact", { url: string; title: string }>;
// {
// home: { url: string; title: string };
// about: { url: string; title: string };
// contact: { url: string; title: string };
// }
// Readonly<T>:将所有属性变为只读
type ReadonlyTodo = Readonly<Todo>;
// { readonly title: string; readonly description: string; readonly completed: boolean; }
// ReturnType<T>:提取函数返回值类型
function getUser() {
return { id: 1, name: "Alice" };
}
type UserType = ReturnType<typeof getUser>;
// { id: number; name: string; }
// Parameters<T>:提取函数参数类型(元组)
function createUser(name: string, age: number, active: boolean) {}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number, boolean]
实战案例
案例一:类型安全的 API 请求函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 泛型让 API 请求函数的返回值类型安全
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json() as Promise<T>;
}
// 定义接口返回类型
interface User {
id: number;
name: string;
email: string;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
}
// 使用:返回值类型自动推断
const user = await request<User>("/api/users/1");
// user 的类型是 User,可以安全访问 user.name
const posts = await request<Post[]>("/api/posts");
// posts 的类型是 Post[],可以安全遍历
案例二:类型安全的事件发射器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 泛型让事件名和事件数据类型绑定
type EventMap = {
login: { userId: string; timestamp: number };
logout: { userId: string };
message: { from: string; content: string };
};
class TypedEventEmitter<Events extends Record<string, any>> {
private listeners: { [K in keyof Events]?: Array<(data: Events[K]) => void> } = {};
on<K extends keyof Events>(event: K, callback: (data: Events[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners[event]?.forEach(cb => cb(data));
}
off<K extends keyof Events>(event: K, callback: (data: Events[K]) => void): void {
this.listeners[event] = this.listeners[event]?.filter(cb => cb !== callback);
}
}
// 使用:事件名和数据类型严格绑定
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("login", (data) => {
// data 的类型自动推断为 { userId: string; timestamp: number }
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
emitter.emit("login", { userId: "123", timestamp: Date.now() }); // ✅
// emitter.emit("login", { userId: "123" }); // ❌ 缺少 timestamp
// emitter.emit("unknown", {}); // ❌ 未知事件
案例三:泛型实现链式调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 泛型 + this 类型实现类型安全的链式调用
class Builder<T extends Record<string, any> = {}> {
private data: Partial<T> = {};
with<K extends string, V>(key: K extends keyof T ? never : K, value: V):
Builder<T & Record<K, V>> {
(this.data as any)[key] = value;
return this as any;
}
build(): T {
return this.data as T;
}
}
// 使用
const result = new Builder()
.with("name", "Alice")
.with("age", 30)
.with("active", true)
.build();
// result 的类型: { name: string; age: number; active: boolean }
底层原理
TypeScript 泛型的编译结果
泛型是纯编译时特性,编译后所有泛型信息都被擦除(类型擦除):
1
2
3
4
5
6
7
8
9
// 源码
function identity<T>(value: T): T {
return value;
}
// 编译结果(TypeScript → JavaScript)
function identity(value) {
return value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 源码:泛型约束
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(value.length);
}
// 编译结果:约束被擦除,但运行时逻辑不变
function logLength(value) {
console.log(value.length);
}
类型推断算法
TypeScript 使用基于控制流的类型推断来确定泛型参数:
1
2
3
4
5
6
7
8
9
10
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
// 推断过程:
// 1. arr 参数的类型是 ["hello", "world"] → T = string
// 2. fn 参数的类型是 (item: string) => item.length → U = number
// 3. 返回值类型 → number[]
const lengths = map(["hello", "world"], item => item.length);
// lengths: number[]
💡 人话总结:
- 泛型就是给类型留个占位符,用的时候再填
T就像函数的参数,只不过传的不是值,而是类型- 泛型约束
extends就像参数校验,限制可以传哪些类型 - 编译后泛型全部消失,它是纯粹的编译时安全网
高频面试题解析
Q1:什么是泛型?为什么需要泛型?
参考答案: 泛型是一种类型参数化的机制,允许在定义时不指定具体类型,而在使用时再确定。
需要泛型的原因:
- 类型安全:相比
any,泛型保留了完整的类型信息 - 代码复用:一份代码适用于多种类型,避免重复
- 类型推断:TypeScript 能自动推断泛型参数,减少手动标注
- 约束能力:通过
extends限制类型范围,比any更安全
1
2
3
4
5
6
7
// ❌ any:丢失类型信息
function bad(value: any): any { return value; }
const x = bad("hello"); // x: any
// ✅ 泛型:保留类型信息
function good<T>(value: T): T { return value; }
const y = good("hello"); // y: string(自动推断)
Q2:泛型约束 extends 有什么用?举例说明。
参考答案: 泛型约束用于限制泛型参数的类型范围,确保泛型参数满足特定条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 约束为特定接口
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// 2. 约束为 keyof(键约束)
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map(key => obj[key]);
}
const user = { name: "Alice", age: 30, active: true };
pluck(user, ["name", "age"]); // (string | number)[]
// pluck(user, ["email"]); // ❌ "email" 不是 keyof user
// 3. 约束为构造函数
function createInstance<T>(Ctor: new () => T): T {
return new Ctor();
}
Q3:keyof 和泛型结合使用是什么效果?
参考答案: keyof 与泛型结合可以实现类型安全的属性访问:
1
2
3
4
5
6
7
8
9
10
11
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 30 };
// K 被约束为 "name" | "age"
// 返回值类型 T[K] 根据 key 自动推断
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// getProperty(person, "email"); // ❌ 编译错误
这种模式的核心价值:
- 编译时检查:访问不存在的属性会报错
- 自动类型推断:返回值类型随 key 变化
- 重构安全:属性改名时自动报错
Q4:泛型在 React 组件中如何使用?
参考答案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 泛型函数组件
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用
interface User { id: number; name: string; }
<List
items={[{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
// 泛型 Hook
function useLocalStorage<T>(key: string, initialValue: T):
[T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}
总结与扩展
核心要点
- 泛型 = 类型参数化:定义时不确定类型,使用时再指定
- 泛型函数/接口/类:三种泛型定义方式
- 泛型约束 extends:限制类型范围,保证类型安全
- 工具类型:Partial、Pick、Omit、Record 等都是泛型的实战应用
- 类型擦除:泛型是纯编译时特性,运行时不存在
延伸学习方向
- 条件类型:泛型 + 条件判断实现更复杂的类型逻辑
- 映射类型:基于泛型批量转换类型
- infer 关键字:在泛型约束中提取类型
- 协变与逆变:泛型的子类型关系
相关主题
本文由作者按照 CC BY 4.0 进行授权