TypeScript 泛型

泛型(generic)是 TS 里"高级但必修"的概念。听起来唬人,其实核心就一句话:把类型也当成参数。就像函数用形参接收不同的值,泛型用类型参数(通常叫 T)接收不同的类型,让你写出"对任何类型都通用"的代码。这一章把它讲透。

1. 为什么需要泛型?

先看一个问题:写一个"取数组第一个元素"的函数。如果不用泛型,只能用 any,等于白写 TS:

// 问题:写一个"取数组第一个元素"的函数
// 如果不用泛型,只能用 any(失去类型检查)
function firstAny(arr: any[]): any {
  return arr[0];
}
const n = firstAny([1, 2, 3]);   // n 是 any,没有类型保护

// 用泛型:<T> 是"类型参数",调用时再确定具体类型
function first<T>(arr: T[]): T {
  return arr[0];
}

const a = first([1, 2, 3]);        // T 推断为 number,a 是 number
const b = first(["a", "b"]);        // T 推断为 string,b 是 string
const c = first([true, false]);     // T 推断为 boolean,c 是 boolean

关键看 <T> 这个尖括号——它声明了一个类型参数 T。调用 first([1,2,3]) 时,TS 看到参数是 number 数组,就把 T 替换成 number,于是返回值 a 就是 number,有了完整的类型保护。

2. 理解 T:类型的占位符

T 想象成"占位符":写函数时不固定具体类型,调用时再确定。T 只是约定俗成的名字,可以叫任何名字:

// 把 T 想象成"占位符":写函数时不固定具体类型
// 调用时,TS 根据你传入的参数,自动推断 T 是什么
function identity<T>(value: T): T {
  return value;
}

// 也可以显式指定 T(通常让 TS 推断即可)
identity<string>("hello");   // 显式指定 T = string
identity(42);                // 推断 T = number

// T 只是约定俗成的名字,可以叫任何名字
// 常见: T(Type)、K(Key)、V(Value)、E(Element)、R(Return)
function mapValue<K, V>(key: K, value: V): V {
  return value;
}

3. 多个类型参数

泛型不只能有一个参数,可以同时有多个(比如键值对场景):

// 多个类型参数
function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}
const p = pair("name", 20);   // [string, number]

4. 泛型接口(重点)

泛型接口是实战中最常见的形态——尤其是API 响应、通用容器、状态管理。掌握它,你就能看懂大部分开源库的类型定义:

// 泛型接口:常用于"通用容器""通用仓库"
interface Box<T> {
  value: T;
  label: string;
}

const box1: Box<number> = { value: 42, label: "数字盒" };
const box2: Box<string> = { value: "hi", label: "字符串盒" };

// 实战:分页响应是经典的泛型接口
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

const userRes: ApiResponse<{ name: string }> = {
  code: 200,
  message: "ok",
  data: { name: "小明" },
};

ApiResponse<T> 这个模式在前端项目里几乎天天用:后端返回的数据结构是固定的(code、message、data),但 data 的具体形状因接口而异——用泛型 T 表示 data 的类型,完美。

5. 泛型类

类也可以有类型参数,常用于通用数据结构(栈、队列、集合):

// 泛型类
class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
const top = numStack.pop();   // number | undefined

const strStack = new Stack<string>();
strStack.push("a");

6. 泛型约束:extends

默认情况下,T 可以是任何类型。但有时你需要限制它必须满足某些条件——比如"必须有 length 属性"。这时用 extends 做约束:

// 泛型约束:用 extends 限制 T 必须满足某些条件
interface HasLength {
  length: number;
}

// T extends HasLength 表示:T 至少要有 length 属性
function logLength<T extends HasLength>(arg: T): void {
  console.log(arg.length);
}
logLength("hello");      // ✅ string 有 length
logLength([1, 2, 3]);     // ✅ 数组有 length
// logLength(42);         // ❌ number 没有 length

// keyof 约束:K 必须是 T 的某个键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "小明", age: 20 };
getProperty(user, "name");   // string
getProperty(user, "age");    // number
// getProperty(user, "xxx"); // ❌ "xxx" 不是 user 的键

其中 keyof T(T 的所有键的联合)是非常高频的搭配,用于"安全访问对象属性"。上面 getProperty 函数:传不存在的键会直接报错,避免了运行时的 undefined bug。

7. 默认类型参数

泛型参数可以有默认值,不指定时使用默认:

// 默认类型参数:不指定时用默认值
interface Paginated<T = string> {
  items: T[];
}
const p1: Paginated = { items: ["a", "b"] };        // T 默认 string
const p2: Paginated<number> = { items: [1, 2] };    // T 显式 number

小结

泛型的本质:把类型当参数。掌握三个要点——<T> 声明类型参数、T[]/Box<T> 使用类型参数、extends 给类型参数加约束。新手先学会读泛型、会用简单泛型函数/接口;看到框架里复杂的 <T extends ...> 再慢慢消化,不用一上来就全懂。

← 上一篇 TypeScript 函数

下一篇 TypeScript 联合与交叉类型

✈️💬