类型参数是"类型的函数": 调用时推断, 约束里限权, 条件类型里递归 — 但只活在编译期, 运行时一片空白
泛型就是类型的形参: 函数 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>([]); // 推不动时显式填
function len<T extends { length: number }>(x: T) { return x.length; // string/数组/自定义都行 }
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, 精确到键
interface Res<D = unknown, E = Error> { code: number; data?: D; error?: E; } Res; // data 是 unknown Res<string>; // error 仍是 Error
type IsStr<T> = T extends string ? true : false; IsStr<'a'>; // → true IsStr<number>; // → false
type Elem<A> = A extends (infer E)[] ? E : never; Elem<boolean[]>; // → boolean type Ret<F> = F extends (...a: any[]) => infer R ? R : never;
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 }
function keysOf<const T>(o: T) { return Object.keys(o); } keysOf({ a: 1, b: 2 }); // → ('a' | 'b')[] 而不是 string[]
// tsc 产物里: // type/interface/泛型参数 全部不见, 只剩 JS
function parse(s: 'num'): number; function parse(s: 'str'): string; function parse(s: string): unknown { /* ... */ }
// 错: function f<T>(v: T): T { return v; } // 等价 any 化 // 对: 直接写具体类型, 或干脆别抽这层
裸 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'); // ✗ 拼错直接编译报错
每个实体一套重复 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');
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 }); // ✗ 载荷不对编译报错
测试里只想覆盖配置的一层子树。递归条件类型实现深度可选:
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 } }, // 只写关心的三层 };
列表组件的列定义要与数据行类型对应, 泛型 + 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 }]} />
老代码手写 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' 编译期报错
异常靠运行时, 业务分支靠类型。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 }
错误对象字段名写错查半天。校验器泛型化, 错误键 = 表单键:
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; }
用 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 完全一致
泛型工厂需要 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);
// 错: function make<T>(): T { return new T(); } // 对: function make<T>(ctor: new () => T): T { return new ctor(); }
// 分发: T extends U ? X : Y // 不分发: [T] extends [U] ? X : Y
type IsNever<T> = [T] extends [never] ? true : false; IsNever<never>; // → true 直接写 T extends never 永远 false
// 错: function f<T>(v: T) { return v.foo; } // T 上没 foo // 对: function f<T extends { foo: unknown }>(v: T) { return v.foo; }
// 对: parse(s: 'num'): number; parse(s: 'str'): string; // parse(s: string): unknown; // 兜底永远放最后
// 错: class Box<T> { static e: T; } // 对: class Box<T> { static e<T>(): Box<T> { ... } }
// 过紧: T extends User 当只需要 { id: number } // 最小: T extends { id: number }
// 尾递归形式让 TS 复用栈帧: type Join<A extends string[]> = A extends [string, ...infer R] ? Join<R> : string;
<T,> 或 <T extends unknown>。 // 错: const f = <T>(v: T) => v; // tsx 里被当 JSX // 对: const f = <T,>(v: T) => v;
const xs = []; // any[] → 严格模式报错 const xs: number[] = [];
// 错: as User 一标了事, 运行时可能缺字段 // 对: UserSchema.parse(json) 先校验后当 User 用
type Elem<A extends readonly unknown[]> = A[number];
const cfg = { port: 8080 } satisfies Config; cfg.port; // 字面量 8080 被保留, not just number
// 底线: 每个 as 都要能回答"为什么这里安全"type Weird<T> = T extends true ? 1 : 0; Weird<boolean>; // → 1 | 0 (true/false 各算一次)
// T extends {id:number} 不会给数据补 id // 对: 入口 assert('id' in obj)
// 错: function g<T>(o: T): Record<string, unknown> // 对: function g<T>(o: T): Pick<T, KeysOfT> 形式保留关联
interface A<T = string> {} interface B extends A {} // B 用时默认还是 string, 容易误解
// 参数位置的 infer 捕获受逆变影响 // 用例: type _ = Expect<Equal<X, Y>> 锁死
// T 在签名里只出现 1 次且只有一个调用点 → 别泛型