文章

泛型基础与应用场景深度解析

深入解析TypeScript泛型函数、泛型约束与常用泛型工具类型的核心原理与实战应用

泛型基础与应用场景深度解析

一句话概括

泛型是 TypeScript 的类型参数化能力——让你写一份代码,适应多种类型,同时保持完整的类型安全。

背景

在实际开发中,我们经常遇到这样的问题:

  • 一个函数需要同时支持 stringnumber 参数
  • 一个工具类需要处理不同类型的数据结构
  • 一个 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:什么是泛型?为什么需要泛型?

参考答案: 泛型是一种类型参数化的机制,允许在定义时不指定具体类型,而在使用时再确定。

需要泛型的原因:

  1. 类型安全:相比 any,泛型保留了完整的类型信息
  2. 代码复用:一份代码适用于多种类型,避免重复
  3. 类型推断:TypeScript 能自动推断泛型参数,减少手动标注
  4. 约束能力:通过 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];
}

总结与扩展

核心要点

  1. 泛型 = 类型参数化:定义时不确定类型,使用时再指定
  2. 泛型函数/接口/类:三种泛型定义方式
  3. 泛型约束 extends:限制类型范围,保证类型安全
  4. 工具类型:Partial、Pick、Omit、Record 等都是泛型的实战应用
  5. 类型擦除:泛型是纯编译时特性,运行时不存在

延伸学习方向

  • 条件类型:泛型 + 条件判断实现更复杂的类型逻辑
  • 映射类型:基于泛型批量转换类型
  • infer 关键字:在泛型约束中提取类型
  • 协变与逆变:泛型的子类型关系

相关主题

本文由作者按照 CC BY 4.0 进行授权