文章

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

泛型是类型的'函数参数',把类型也变成可传递的变量,一份代码处理多种类型且不丢失精度。 面试能否写出带泛型的工具函数,直接决定对你 TS 水平的定位。

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

一句话概括

泛型 = 类型的”函数参数”——把类型也变成可传递的变量,一份代码处理多种类型而不丢失精度。TypeScript 面试中,能不能写出带泛型的工具函数,直接决定了面试官对你 TS 水平的定位。

核心知识点

1. 泛型函数:让类型”流过”函数不丢失

1
2
3
4
5
6
7
8
9
10
// ❌ any 方案:入参是 string,返回值丢了类型信息
function identity(x: any): any { return x; }
const a = identity("hello");
a.toUpperCase(); // 不报错,但 a 的类型是 any,没有智能提示

// ✅ 泛型:调用时自动推断,返回值精准保留
function identity<T>(x: T): T { return x; }
const b = identity("hello");   // b: "hello"(字面量类型!)
const c = identity(42);        // c: 42
const d = identity([1, 2, 3]); // d: number[]

面试要点:TypeScript 的类型推断比你想象的更激进——传字面量推断为字面量类型,传对象推断为对象字面量类型。如果你期望更宽的类型(如 string 而非 "hello"),手动标注泛型参数:identity<string>("hello")。

2. 泛型约束 extends:不只是”限制”,更是”赋予能力”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 问题:想访问 T 的某个属性,但 TS 不知道 T 有
function getLength<T>(x: T): number {
  // return x.length; // ❌ T 上不存在 length
}

// 解决:extends 让 T 获得被约束类型的所有属性
function getLength<T extends { length: number }>(x: T): number {
  return x.length; // ✅ TS 知道 T 一定有 length
}

getLength("hello");        // 5
getLength([1, 2, 3]);      // 3
getLength({ length: 10 }); // 10
// getLength(42);          // ❌ number 没有 length

约束的核心价值不是”限制能传什么”,而是”告诉 TS 这个泛型参数拥有哪些能力”——extends 之后,TS 就知道 T 上有 length,你才能在函数体内安全访问它。

3. 泛型接口:API 响应层的标准范式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 后端统一响应格式 → 前端用泛型接口建模,data 字段自动跟随业务类型
interface ApiResponse<T> {
  code: number;
  data: T;
  message: string;
}

interface User { id: number; name: string; email: string; }
interface Order { orderId: string; amount: number; }

declare function fetchUser(): Promise<ApiResponse<User>>;
declare function fetchOrder(): Promise<ApiResponse<Order>>;

const userRes = await fetchUser();
userRes.data.email; // ✅ 自动补全,类型为 string

const orderRes = await fetchOrder();
orderRes.data.amount; // ✅ 自动补全,类型为 number

4. 泛型 + keyof:编译期保障属性名不写错

1
2
3
4
5
6
7
8
9
10
// 不用 any、不用索引签名,依然能访问任意属性
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", isActive: true } as const;

getProperty(user, "name");     // 返回 "Alice",类型为 "Alice"
getProperty(user, "isActive"); // 返回 true,类型为 true
// getProperty(user, "email"); // ❌ 编译报错——不存在就是不存在

5. 泛型类与泛型 Hook

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
// 泛型类:类型安全的数据容器
class DataCache<T> {
  private store = new Map<string, T>();

  set(key: string, value: T): void {
    this.store.set(key, value);
  }
  get(key: string): T | undefined {
    return this.store.get(key);
  }
}

const userCache = new DataCache<User>();
userCache.set("alice", { id: 1, name: "Alice", email: "a@b.com" });
const u = userCache.get("alice"); // User | undefined ✅

// 泛型 Hook:localStorage 读写自动跟进类型
function useStoredState<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initial;
    } catch {
      return initial;
    }
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

其实你每天都在用

  1. useState<User | null>(null):那个尖括号 <> 就是泛型,告诉 TS state 是 User | null
  2. axios.get<User[]>(url):返回值从 any 变成 User[],IDE 里 .data[0].name 一路补全
  3. Array.prototype.map<U>():[1,2,3].map(String) 返回 string[],因为 map 的签名是 map<U>(callback: (v: T) => U): U[]
  4. Ant Design 表格:const columns: ColumnsType<User>[] = [...] → dataIndex 只能是 User 的 key,写错立即红线
  5. Promise.all<T[]>():Promise.all([fetchUser(), fetchCount()]) 自动推导出 [User, number]

常见误解(FAQ)

❌ 误区 1:「any 和泛型差不多,只是泛型更优雅」

差远了。any 是”关掉类型检查”,泛型是”推迟类型确定但最终保证安全”。function echo(x: any): any 的调用方拿到的永远是 any——编辑器不补全、重构不报错;而 echo<T>(x: T): T 的调用方拿到的是精确类型,你重构改了 User 的字段名,所有调用处都会亮红线提醒你。

❌ 误区 2:「泛型参数必须显式传 <T>」

多数情况不需要。TS 的类型推断从函数实参自动推导 T——你传 [1, 2, 3],T 就是 number。只有推断失败(空数组 [] 推断为 never[])或你想故意放宽类型时,才显式写 <string | number>([])。

❌ 误区 3:「T extends {} 和 T extends object 意思一样」

{} 匹配除 null / undefined 外的所有类型(包括 string、number 等原始类型);object 只匹配引用类型(对象、数组、函数)。function fn<T extends object>(x: T) 不接受 fn("hello"),后者直接报错。面试把这个讲清楚,TS 水平直接升一档评价。

❌ 误区 4:「泛型编译后还有运行时开销」

TypeScript 的泛型是纯编译时机制,编译后类型全部擦除——泛型类/函数编译出来的 JS 和不写泛型完全一样。这是 TS “零运行时成本” 的设计哲学,和 Java(泛型擦除但有桥方法)或 C#(运行时保留泛型信息)不同。

一句话总结

泛型是 TypeScript 类型系统从”给 JS 贴标签”升级到”类型可编程”的入口——你把 T 想象成类型的参数,把 extends 想象成参数校验,所有泛型工具类型就都通了。

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