TypeScript · 泛型与类型编程

类型参数是"类型的函数": 调用时推断, 约束里限权, 条件类型里递归 — 但只活在编译期, 运行时一片空白

调用即实例化: T 被当场推断 function identity<T>(v: T): T { return v; } identity(42) T 被推断为 number → 签名特化 (v: number) => number 编译完成后: identity(42) 就是普通 JS 函数 T 只活在这两行之间 — 一到运行时即消失 约束限权, infer 提取 function idOf<T extends { id: number }>(e: T) { return e.id; // 没约束这里直接报错 extends 画下"最小能力边界": 够用才能进 type Unwrap<P> = P extends Promise<infer U> ? U : P; Unwrap<Promise<string>> // → string 条件类型 + infer = 类型层的"模式匹配与解构" 工具类型 Pick/Omit/ReturnType 全是它的组合 擦除: 运行时没有 T function make<T>(): T { return new T(); // 编译错! // 运行时没有 T 这个东西 } 同样不行: T instanceof X / T 作值用 解法: 把"造东西的能力"作为参数传进来 function make<T>( ctor: new () => T): T { return new ctor(); } // ✓ // 分布式条件类型: 裸类型参数遇上联合会"分发" — 最反直觉的一页 type ToArray<T> = T[]; ToArray<string | number> // → (string | number)[] 一个数组 type ToArr2<T> = T extends any ? T[] : never; ToArr2<string | number> // → string[] | number[] 分发成两个! // 想不分发: 用元组包住 T, 让它不再是"裸"类型参数 type NoDist<T> = [T] extends [any] ? T[] : never; NoDist<string | number> // → (string | number)[] 又回到一个

调用即实例化

  • • 泛型是"类型的函数", 调用时填参
  • • 能推断就推断, 推不动才显式标
  • • 返回类型跟着入参走, 改一处全联动
  • • 编译后类型全部消失

约束与 infer

  • • extends 画最小能力边界
  • • 条件类型 = 类型层的三元表达式
  • • infer 在匹配位置"捕获"子类型
  • • keyof/索引访问是约束的好搭档

擦除是底线

  • • 不能 new T / instanceof T
  • • 运行时校验另请 zod 等库
  • • 需要造对象: 构造签名参数传入
  • • 泛型只约束"编译期形状"

💡 一句话理解

泛型就是类型的形参: 函数 identity<T>(v: T): T 是一个"类型的函数", 你每调用一次, TS 就根据实参把 T 填上, 现场推导出精确的签名 — identity(42) 得到 number 版本, identity('a') 得到 string 版本, 一份逻辑处处精确。约束 T extends X 则是"报名资格": 想当 T, 至少要有 X 的能力, 函数体内才准用 X 的成员。

往上走一层是条件类型: 类型世界的 if/else, 配合 infer 能在匹配中"捕获"嵌套类型(Promise 的返回值、数组的元素、函数的参数) — Awaited、ReturnType 全是这么造出来的。但记住底线: 泛型只活在编译期, 运行时既没有 T 也没有类型系统, 需要运行时保证请回到校验器。

🧠 必知必会 必考 & 必会

泛型函数与推断
类型参数从实参推断; 显式标注只在推断不动点时用(空数组/联合歧义)。
function first<T>(arr: T[]): T { return arr[0]; }
first([1, 2]);        // → number  推断成功
first<string>([]);    // 推不动时显式填
约束 extends
限定 T 的最小形状; 约束之后函数体才能安全访问对应成员。
function len<T extends { length: number }>(x: T) {
  return x.length;   // string/数组/自定义都行
}
keyof 与索引访问
keyof T 拿键联合, T[K] 按键取类型 — 类型安全的动态属性访问。
function get<T, K extends keyof T>(o: T, k: K): T[K] {
  return o[k];
}
get({ a: 1, b: 'x' }, 'b');  // → string, 精确到键
默认类型参数
<T = string> 省略时兜底; 常用于容器/配置型泛型, 降低调用方负担。
interface Res<D = unknown, E = Error> {
  code: number; data?: D; error?: E;
}
Res;                // data 是 unknown
Res<string>;         // error 仍是 Error
条件类型
类型层三元: T extends U ? X : Y; 判断的是"可赋值性", 配 infer 才有解构能力。
type IsStr<T> = T extends string ? true : false;
IsStr<'a'>;    // → true
IsStr<number>; // → false
infer 提取
在 extends 的模式位置声明待捕获变量; 支持嵌套/数组/函数参数等位置。
type Elem<A> = A extends (infer E)[] ? E : never;
Elem<boolean[]>;     // → boolean
type Ret<F> = F extends (...a: any[]) => infer R ? R : never;
分布式条件类型
裸类型参数遇到联合自动分发: 每个成员各算一次再联合; [T] 元组包住可阻断。
type ToArr<T> = T extends any ? T[] : never;
ToArr<A | B>;        // → A[] | B[] (分发了)
NoDist<A | B>;       // 元组包裹后不分发
泛型接口与类
类/接口的类型参数作用于实例成员; 静态成员用不了类的类型参数。
class Box<T> {
  v?: T;                       // ✓ 实例成员
  // static empty: T;  ✗ 编译错
  static empty<T>(): Box<T> { return new Box(); }  // ✓ 自己带 T
}
const 类型参数
<const T> 让推断偏向"最窄字面量"(TS 5.0+), 配 satisfies 保留字面精度。
function keysOf<const T>(o: T) { return Object.keys(o); }
keysOf({ a: 1, b: 2 });   // → ('a' | 'b')[] 而不是 string[]
擦除
编译后类型系统整体消失: 没有 T、没有 interface、没有枚举检查; 运行时正确性靠校验库。
// tsc 产物里:
// type/interface/泛型参数 全部不见, 只剩 JS
泛型 vs 重载
多形态输入输出时: 重载列"签名谱", 泛型表达"输入决定输出"的映射; 二者可叠加。
function parse(s: 'num'): number;
function parse(s: 'str'): string;
function parse(s: string): unknown { /* ... */ }
别过度泛型
只有一个调用点、T 只出现一次的"泛型"是负担; 先写具体, 出现第二形态再抽象。
// 错: function f<T>(v: T): T { return v; }  // 等价 any 化
// 对: 直接写具体类型, 或干脆别抽这层

🏭 生产实战 real world

场景 1 · 类型安全 API 客户端: 响应类型跟着 URL 走

裸 fetch 返回 any, 每处都要 as。用端点映射表把 URL 与响应类型绑定:

type Endpoints = {
  '/users': User[];
  '/orders': Order[];
};
async function get<P extends keyof Endpoints>(path: P) {
  const r = await fetch(base + path);
  return (await r.json()) as Endpoints[P];  // 边界处唯一一次 as
}
const users = await get('/users');   // users: User[] 自动得出
get('/user');                         // ✗ 拼错直接编译报错

场景 2 · 泛型仓储: 一套 CRUD 模板复用到所有表

每个实体一套重复 DAO。抽取泛型基类, 实体类型与 id 键名都参数化:

class Repo<T extends { id: number }> {
  constructor(private db: DB, private table: string) {}
  find(id: number): Promise<T | undefined> {
    return this.db.one(`select * from ${this.table} where id=$1`, id);
  }
  save(e: T) { /* ... */ }
}
const users = new Repo<User>(db, 'users');

场景 3 · 类型安全事件总线: 事件名与载荷强关联

on('pay') 的回调却拿到 shipping 载荷, 泛型映射让每个事件名只接受自己的载荷:

type Events = { pay: PayPayload; login: { userId: string } };
class Bus<E extends Record<string, unknown>> {
  #h = new Map();
  on<K extends keyof E>(k: K, fn: (p: E[K]) => void) { /* ... */ }
  emit<K extends keyof E>(k: K, p: E[K]) { /* ... */ }
}
bus.on('pay', p => p.amount);      // p 自动是 PayPayload
bus.emit('pay', { wrong: 1 });     // ✗ 载荷不对编译报错

场景 4 · DeepPartial: 递归可选的工具类型

测试里只想覆盖配置的一层子树。递归条件类型实现深度可选:

type DeepPartial<T> = T extends (infer I)[] ? DeepPartial<I>[]
  : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;
const overrides: DeepPartial<Config> = {
  db: { pool: { max: 5 } },      // 只写关心的三层
};

场景 5 · 泛型 React 组件: props 联动 options 联合

列表组件的列定义要与数据行类型对应, 泛型 + keyof 锁死:

type Col<Row, K extends keyof Row> =
  | { key: K; render: (r: Row) => React.ReactNode }
  | { key: K; format?: never };
function Table<Row, K extends keyof Row>(
  props: { rows: Row[]; cols: Col<Row, K>[] }) { /* ... */ }
<Table rows={users} cols={[{ key: 'name', render: u => u.name }]} />

场景 6 · 类型安全 pick: 30 行手写 Pick 的教训

老代码手写 pick 靠 any, 键拼错运行时才炸。keyof 版本编译期全拦:

function pick<T, K extends keyof T>(o: T, keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = o[k];   // 索引访问全类型安全
  return out;
}
pick(user, ['name', 'emial']);  // ✗ 'emial' 编译期报错

场景 7 · Result<T, E>: 把错误塞进类型

异常靠运行时, 业务分支靠类型。Result 双态让"必须处理失败"成为编译期要求:

type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };
function parse(r: Result<string, ValidationError>) {
  if (r.ok) return r.value;     // 这里 r 一定是成功分支
  return r.error.field;          // 这里一定是 error
}

场景 8 · 表单校验错误映射: 键与字段一一对应

错误对象字段名写错查半天。校验器泛型化, 错误键 = 表单键:

type Errors<F> = { [K in keyof F]?: string };
function validate<F extends Record<string, unknown>>(
  form: F, rules: { [K in keyof F]: (v: F[K]) => string | null }
): Errors<F> {
  const errs: Errors<F> = {};
  for (const k in rules) { const m = rules[k](form[k]); if (m) errs[k] = m; }
  return errs;
}

场景 9 · 类型安全的 memoize: 入参与返回联动

用 Parameters/ReturnType 反推被包装函数的签名, 包装器不再丢类型:

function memo<F extends (...args: any[]) => unknown>(fn: F) {
  const cache = new Map<string, ReturnType<F>>();
  return (...args: Parameters<F>): ReturnType<F> => {
    const k = JSON.stringify(args);
    if (!cache.has(k)) cache.set(k, fn(...args) as ReturnType<F>);
    return cache.get(k)!;
  };
}
const fastCalc = memo(calcPrice);  // 签名与 calcPrice 完全一致

场景 10 · 构造签名注入: 擦除的正规解法

泛型工厂需要 new T, 直接写编译不过。把"怎么造"作为参数传:

function hydrate<T>(
  rows: Raw[], ctor: new (raw: Raw) => T ): T[] {
  return rows.map(r => new ctor(r));   // 运行时有 ctor 可用
}
const users = hydrate(rows, User);

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 运行时用 T — new T()/instanceof T 全是编译错, 类型擦除后 T 不存在. 正解: 构造签名或工厂作为参数传入。
// 错: function make<T>(): T { return new T(); }
// 对: function make<T>(ctor: new () => T): T { return new ctor(); }
坑 2 · 条件类型意外分发 — 裸 T 遇联合逐成员分发, 结果"分裂". 正解: [T] extends [U] 元组包住阻断。
// 分发: T extends U ? X : Y
// 不分发: [T] extends [U] ? X : Y
坑 3 · never 在分发里消失 — 裸 never 不参与分发, 条件类型直接得 never. 正解: 同上用 [T] 包裹后检测。
type IsNever<T> = [T] extends [never] ? true : false;
IsNever<never>;   // → true  直接写 T extends never 永远 false
坑 4 · 泛型默认 any 逃逸 — 约束缺失时推断滑向 any/unknown. 正解: 所有公开泛型都写 extends 下界。
// 错: function f<T>(v: T) { return v.foo; }  // T 上没 foo
// 对: function f<T extends { foo: unknown }>(v: T) { return v.foo; }
坑 5 · 重载顺序错误 — 重载自上而下取第一个匹配, 宽签名放前面会吞掉精确分支. 正解: 精确在前, 兜底最后。
// 对: parse(s: 'num'): number; parse(s: 'str'): string;
//      parse(s: string): unknown;  // 兜底永远放最后
坑 6 · 泛型类静态成员用 T — 静态成员属于类不属于实例, 拿不到类型参数. 正解: 静态方法自己声明类型参数。
// 错: class Box<T> { static e: T; }
// 对: class Box<T> { static e<T>(): Box<T> { ... } }
坑 7 · 约束两极化 — 太宽(extends unknown)失去保护, 太紧调用方传不进来. 正解: 按"函数体真正用到的成员"画最小约束。
// 过紧: T extends User 当只需要 { id: number }
// 最小: T extends { id: number }
坑 8 · 递归条件类型爆栈 — 深层递归类型超 50 层实例化上限报错. 正解: 尾递归写法(条件为真的分支放最后)或拆分工具类型。
// 尾递归形式让 TS 复用栈帧:
type Join<A extends string[]> =
  A extends [string, ...infer R] ? Join<R> : string;
坑 9 · .tsx 里 <T> 歧义 — 箭头函数泛型与 JSX 标签冲突. 正解: <T,> 或 <T extends unknown>。
// 错: const f = <T>(v: T) => v;    // tsx 里被当 JSX
// 对: const f = <T,>(v: T) => v;
坑 10 · 推断不动点没显式标 — 空数组/多分支联合推断成 never/宽联合. 正解: 显式类型参数或先声明变量类型。
const xs = [];        // any[] → 严格模式报错
const xs: number[] = [];
坑 11 · 泛型当运行时校验 — 类型对≠数据对, 外部数据仍可乱来. 正解: 边界处 zod 校验, 泛型只管内部契约。
// 错: as User 一标了事, 运行时可能缺字段
// 对: UserSchema.parse(json)  先校验后当 User 用
坑 12 · keyof 数组拿全联合 — keyof number[] 是 number|'length'|方法名... 不只是索引. 正解: 取元素类型用 T[number]。
type Elem<A extends readonly unknown[]> = A[number];
坑 13 · satisfies 顺序 — as 丢推断, satisfies 保字面量只校验; 泛型返回值想窄就用 satisfies。
const cfg = { port: 8080 } satisfies Config;
cfg.port;   // 字面量 8080 被保留, not just number
坑 14 · as 伪造泛型结果 — 内部实现 as 掩盖错误, 类型说谎. 正解: 实现内的 as 集中在边界并写注释; 优先用类型收窄。
// 底线: 每个 as 都要能回答"为什么这里安全"
坑 15 · 分布式里 boolean 拆分 — boolean 在分发时变 true|false 两支, 结果变联合. 正解: 需要整块判断用 [T] 包裹。
type Weird<T> = T extends true ? 1 : 0;
Weird<boolean>;   // → 1 | 0  (true/false 各算一次)
坑 16 · 泛型约束不传运行时 — 约束只帮编译器, 不改变数据. 正解: 需要运行时保证(如必有 id)在入口校验。
// T extends {id:number} 不会给数据补 id
// 对: 入口 assert('id' in obj)
坑 17 · 函数返回宽类型 — 返回 Record<string, any> 等于对外丢类型. 正解: 返回泛型映射结果或具体形状。
// 错: function g<T>(o: T): Record<string, unknown>
// 对: function g<T>(o: T): Pick<T, KeysOfT> 形式保留关联
坑 18 · 泛型默认值遮蔽 — 接口继承后默认类型与父不一致引发意外. 正解: 默认类型显式传递, 不依赖继承链隐式。
interface A<T = string> {}
interface B extends A {}   // B 用时默认还是 string, 容易误解
坑 19 · infer 位置决定方向 — 协变位置(返回值)与逆变位置(参数)捕获行为不同, 复杂 infer 结果反直觉. 正解: 复杂工具类型写用例锁定行为。
// 参数位置的 infer 捕获受逆变影响
// 用例: type _ = Expect<Equal<X, Y>> 锁死
坑 20 · 单点泛型化 — 一处使用就抽泛型, 抽象成本大于收益. 正解: 至少两个真实形态再泛型; "T 出现一次"原则。
// T 在签名里只出现 1 次且只有一个调用点 → 别泛型