跳到主要内容

什么是泛型?如何使用?

下文默认基于 TypeScript 5.x,并假设开启 strict: true

面试速答(30 秒版 TL;DR)

  • 泛型(Generics)就是把“类型”也当成参数,让同一套逻辑适配多种具体类型。
  • 它解决的核心问题是:既要复用代码,又要保留输入和输出之间的类型关系
  • 最典型的写法是:
function identity<T>(value: T): T {
return value;
}
  • 面试里一句话总结:
    • any 是直接放弃检查;
    • 泛型是先不写死类型,但保留类型约束和推导能力。

心智模型:把“变化的类型”提成参数

1. 为什么需要泛型

1.1 不用泛型,类型关系很容易丢

function getFirst(arr: any[]) {
return arr[0];
}

const result = getFirst(["a", "b"]); // any

问题不是代码不能跑,而是:

  • 输入明明是字符串数组;
  • 输出明明应该是字符串;
  • 结果却退化成了 any

1.2 用泛型后,类型会随着输入一起流动

function getFirst<T>(arr: T[]): T {
return arr[0];
}

const a = getFirst(["a", "b"]); // string
const b = getFirst([1, 2, 3]); // number

这里最关键的是:

  • 参数用了 T[]
  • 返回值也用了 T
  • 编译器就能知道“输入是什么,输出也该跟着是什么”。

2. 泛型最常出现在哪些位置

2.1 泛型函数

function identity<T>(value: T): T {
return value;
}

这是最基础、最高频的形式。

2.2 泛型接口

interface ApiResponse<T> {
code: number;
message: string;
data: T;
}

const userRes: ApiResponse<{ id: number; name: string }> = {
code: 0,
message: "ok",
data: { id: 1, name: "Tom" },
};

适合描述:

  • 接口返回结构;
  • 分页数据;
  • 通用状态容器。

2.3 泛型类型别名

type Nullable<T> = T | null;
type Box<T> = { value: T };

类型别名常和联合类型、条件类型、映射类型配合使用。

2.4 泛型类

class Queue<T> {
private items: T[] = [];

push(item: T) {
this.items.push(item);
}

shift(): T | undefined {
return this.items.shift();
}
}

这类容器模型很适合用泛型表达。

3. 泛型约束怎么用

不是所有 T 都能随便访问属性:

function getLength<T>(value: T) {
return value.length; // 报错
}

如果你要访问 length,就必须告诉编译器这个 T 至少有什么能力:

type HasLength = {
length: number;
};

function getLength<T extends HasLength>(value: T) {
return value.length;
}

此时 T extends HasLength 的意思是:

  • T 不再是任意类型;
  • 而是至少拥有 length: number 的类型。

4. 泛型和 keyof 的经典组合

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { id: 1, name: "Tom" };
const name = getProp(user, "name"); // string

这道题是面试高频题,答法可以拆成三句:

  • T 表示对象整体类型;
  • K extends keyof T 保证 key 必须合法;
  • T[K] 表示该 key 对应的属性值类型。

5. 泛型参数可以有多个,也可以有默认值

type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };

这说明泛型不是只能写一个 T,而是可以:

  • 多个类型参数协作;
  • 给部分参数设置默认类型。

6. 泛型通常可以自动推导

很多时候不用手写 <T>

function wrap<T>(value: T) {
return { value };
}

const result = wrap("hello");

这里 T 会自动推导成 string

只有在这些场景,才更常需要显式写类型参数:

  • 推导结果不够准;
  • 需要主动约束调用方式;
  • 某些 API 设计上要求调用方传入明确类型。

7. 泛型和联合类型、any 有什么区别

7.1 和联合类型的区别

  • 联合类型描述“已知几种可能”;
  • 泛型描述“同一套规则适用于很多类型”。
function first(arr: string[] | number[]) {
return arr[0];
}

这不是泛型,它只能处理少数几个固定分支。

7.2 和 any 的区别

  • any:放弃类型检查;
  • 泛型:延迟确定具体类型,但不丢掉类型关系。
function identityAny(value: any): any {
return value;
}

function identityGeneric<T>(value: T): T {
return value;
}

后者明显更安全,也更利于推导。

8. 高频面试题标准答法

8.1 什么是泛型

泛型就是把类型参数化,让同一份逻辑在不同类型下复用,同时保持类型信息不丢。

8.2 泛型的核心价值是什么

三个词就够:

  • 复用;
  • 精确;
  • 可推导。

8.3 泛型是不是越多越好

不是。

好的泛型设计应该满足:

  • 类型参数真的有意义;
  • 参数之间存在清晰关系;
  • 调用方大多数时候不用显式手写一长串类型参数。

如果一个 API 需要用户手写一堆 <T, U, K, R, P> 才能用,通常说明设计已经开始失控。

9. 常见误区

  • 把泛型当成 any 的高级写法。泛型不是放弃检查,而是延迟具体化。
  • 类型参数只出现一次,却硬上泛型。这种设计很多时候没有价值。
  • 忘了加约束,就直接访问 T 上的属性。
  • 以为泛型能做运行时类型判断。实际上运行时拿不到 T

速记要点

  • 泛型 = 类型参数化。
  • 目标:复用逻辑,同时保留类型关系。
  • 常见位置:函数、接口、类型别名、类。
  • 常见组合:extendskeyof、条件类型。