第 11 章 · 泛型工程应用
- 把第 5-8 章的类型能力用到真实工程:工具库、组件、API 封装
- 学会「类型驱动 API 设计」:让调用方少写类型、多拿保障
- 掌握几个高频的泛型工程模式:约束回填、键值推导、解析器、递归结构
- 通过 5 道 type-challenges 题把「工程思维」落到类型层
11.1 模式一:约束即文档(constraint as contract)
泛型约束不只是「能过编译」,它本身就是给调用方的说明书:
// 约束写清楚了:T 必须是个「键是 string、值是 unknown 的对象」
function toEntries<T extends Record<string, unknown>>(obj: T): [keyof T, unknown][] {
return Object.entries(obj) as [keyof T, unknown][]
}
调用方一眼看出「我能传什么、拿回什么」。约束越精确,文档越清晰。
约束 K extends keyof T + 返回值 T[K] 是最经典的「约束回填」组合——输入约束了 K,输出就自动精确到对应类型:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>
for (const k of keys) result[k] = obj[k]
return result
}
const user = { name: 'ada', age: 30, admin: true }
const picked = pick(user, ['name', 'admin'])
// 类型自动是 { name: string; admin: boolean } —— 精确!
11.2 模式二:让调用方「零标注」
好的泛型 API,调用方几乎不用写类型——全靠推断。看两个反例 vs 正例:
// ❌ 反面:把类型参数放在「推断不到」的位置
function get<T>(): T { throw new Error() }
const x = get() // T 推断不了 → 必须显式 get<number>(),啰嗦
// ✅ 正面:类型参数从「实参」里推断出来
function identity<T>(value: T): T { return value }
const y = identity(42) // T 自动 = number
原则:类型参数尽量放在「和实参相关」的位置,让 TS 从调用方式推出它。
框架里的例子——「按事件名拿回调参数」:
// 事件表:事件名 → 回调参数类型
interface Events {
click: { x: number; y: number }
submit: { form: FormData }
}
function on<E extends keyof Events>(name: E, cb: (payload: Events[E]) => void): void {
// ...
}
on('click', (e) => {
e.x // ✅ number —— 没写任何类型,全靠推断
e.form // ❌ click 事件没有 form,编译期拦下
})
这就是「键值推导」模式:用 keyof 约束事件名,用 Events[E] 回填参数类型。事件系统、状态机、表单注册都靠它。
11.3 模式三:API 封装的类型推导
封装请求库时,最值钱的是「入参类型和出参类型绑定」:
// 接口定义:路径 → 返回类型
interface API {
'/users': User[]
'/users/:id': User
'/me': User
}
async function get<Path extends keyof API>(path: Path): Promise<API[Path]> {
const res = await fetch(path)
return res.json()
}
const users = await get('/users') // Promise<User[]> ✅
const me = await get('/me') // Promise<User> ✅
await get('/unknown') // ❌ 路径不在 API 里
同一个套路还能做请求参数校验:
type RequestMap = {
list: { query: { page: number } }
create: { body: { title: string } }
}
function request<K extends keyof RequestMap>(k: K, opts: RequestMap[K]): void {
// ...
}
request('list', { query: { page: 1 } }) // ✅
request('list', { query: { page: 'x' } }) // ❌ page 必须是 number
一份 RequestMap / API 接口同时是:文档(能调哪些接口)、参数约束(query/body 该长什么样)、返回类型。一处定义,处处推导。
11.4 模式四:类型层「查找」与「解析」
工程里常见「从一个结构里找出/解析出某类信息」的需求,类型层面同样能做。
11.4.1 查找(lookup):从联合里挑出带某字段的成员
interface Cat { type: 'cat'; breeds: string }
interface Dog { type: 'dog'; color: string }
type Animal = Cat | Dog
// 挑出「type 是 T」的那个成员 —— 分布式条件类型
type LookUp<U, T> = U extends { type: T } ? U : never
type DogType = LookUp<Animal, 'dog'> // Dog
这和 00062 TypeLookup 是同一题。它在可辨识联合(第 4 章)之上做「按字段筛选类型」。
11.4.2 解析(parser):从字符串里拆出信息
百分比解析 '+100%' → ['+', '100', '%'],用模板 + infer 拆:
type PercentageParser<A extends string> =
A extends `${infer Sign}${infer Rest}`
? Sign extends '+' | '-'
? Rest extends `${infer Num}%` ? [Sign, Num, '%'] : [Sign, Rest, '']
: A extends `${infer Num}%` ? ['', Num, '%'] : ['', A, '']
: ['', '', '']
type R = PercentageParser<'+100%'> // ['+', '100', '%']
type R2 = PercentageParser<'42'> // ['', '42', '']
解析器的通用形态:先拆「可辨识部分」(正负号),再拆「内容」,最后拆「后缀」。01978 PercentageParser、09616 ParseUrlParams、00151 QueryStringParser 全是这个套路。
11.4.3 递归结构:树与嵌套
遍历一棵树、或把元组嵌套成对象,是递归类型的主场:
// 元组 → 嵌套对象
type TupleToNestedObject<T extends unknown[], U> =
T extends [infer F, ...infer R]
? { [K in F & string]: TupleToNestedObject<R, U> }
: U
type R = TupleToNestedObject<['a', 'b'], number>
// { a: { b: number } }
// 二叉树中序遍历(00033/03376 的模型)
interface TreeNode { val: number; left: TreeNode | null; right: TreeNode | null }
type InorderTraversal<T extends TreeNode | null> =
T extends TreeNode
? [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
: []
00062 = 查找 · 01978 = 解析 · 03326 = 模板拼接 · 03188 = 递归嵌套 · 03376 = 树递归。把工程里遇到的「从 X 推出 Y」,翻译成类型层面的一次递归/一次分发,就是这些题。
本章小结
- 泛型约束 = 给调用方的说明书;「约束回填」让输出自动精确
- 好的泛型 API 调用方零标注:类型参数放「和实参相关」的位置
- 键值推导(
keyof+T[K])驱动事件表、请求封装、表单注册 - 类型层「查找」靠分布式条件,「解析」靠模板 + infer,「递归结构」靠递归条件
- 5 道挑战题 = 这些模式的类型层裸体版本
本章练习
挑战题
| 题号 | 题目 | 难度 | 考察点 |
|---|---|---|---|
| 00062 | 查找类型 | medium | 分布式条件筛选 |
| 03376 | InorderTraversal | medium | 树递归 |
| 01978 | Percentage Parser | medium | 模板解析 |
| 03326 | BEM style string | medium | 模板 + 联合展开 |
| 03188 | Tuple to Nested Object | medium | 递归嵌套 |
代码练习
-
约束回填:实现
pick(obj, keys)返回Pick<T, K>(11.1 的版本),验证返回值类型随keys自动精确。 -
事件表:定义一个
UIEvents接口(至少 3 个事件,各有不同 payload),用on(name, cb)封装,验证cb参数类型按事件名自动匹配、传错事件名报错。 -
请求封装:定义
API接口(3 个路径 + 返回类型),封装get<Path>,验证返回类型随路径变化。 -
百分比解析:不照抄 11.4.2,自己写
PercentageParser,覆盖''、'+'、'-1'、'100%'、'+100%'五个用例。 -
递归工具:写
type DeepPartial<T>(对象的所有属性(含嵌套)都变成可选),验证对{ a: { b: { c: number } } }得到嵌套可选的类型。(提示:映射 + 递归 + 条件,第 13 章会系统讲递归。)