JAVASCRIPT与TYPESCRIPT · KNOWLEDGE CHAPTER
K11

泛型与类型运算

泛型不是给变量换成字母,而是把类型之间的关系写成可复用规则。输入与输出怎样对应、键与值如何配对、回调能处理多宽的输入、联合是逐项变换还是整体判断,才是类型运算要保留的信息。好的抽象使错误更靠近调用点,同时让实现能兑现契约。

JavaScript与TypeScript2 道章节练习4 道主练关联

核心模型

泛型不是给变量换成字母,而是把类型之间的关系写成可复用规则。输入与输出怎样对应、键与值如何配对、回调能处理多宽的输入、联合是逐项变换还是整体判断,才是类型运算要保留的信息。好的抽象使错误更靠近调用点,同时让实现能兑现契约。

原理与机制

泛型保留关系,约束只描述最低能力

identity<T>(x:T):T 把返回值与输入关联,any 则放弃这种信息。get<T,K extends keyof T>(obj:T,key:K):T[K] 让键只能来自对象,并让结果随具体键变化;keyof 是键集合,T[K] 是索引访问类型。T extends {length:number} 只保证可读 length,不赋予函数构造任意 T 的能力:调用者的 T 可能还要求 name。不能用 {length:0} 冒充 T;应返回明确的基础结构、原 T,或让调用者提供真正返回 T 的工厂。

satisfies 用于检查表达式契约

把路由对象直接注解为 Record<string,Route> 容易失去具体键集合;as 断言可能掩盖无法兑现的类型关系。satisfies 检查是否符合目标,同时保留表达式更具体的信息,但它仍可能参与上下文推断,不能说完全不影响推断。配置字面量可用 as const satisfies Record<"home"|"save",Route> 保留路径字面量并检查固定键是否齐全;Record<string,Route> 则不要求这两个键。额外属性检查与新鲜字面量有关,satisfies 不是通用 exact 类型,也不是运行时过滤。

回调可替换性要看消费与生产位置

如果 API 承诺传入任意 Animal,回调必须处理所有 Animal,只接受 Dog 的回调可能对 Cat 调用 bark,因此不安全。strictFunctionTypes 对函数参数的检查体现逆向兼容,而返回值作为生产位置通常可协变;方法语法有兼容性例外,不能用较宽松写法证明逻辑安全。类型参数同时被读写会受到双向约束。Dog[] 被当成可写 Animal[] 后混入 Cat 就是反例;readonly 视图阻止该视图写数组,却不冻结元素或其他可变别名。

条件类型先分清逐项与整体

T extends U ? X : Y 中,被检查的 T 是裸类型参数时,联合常逐成员计算并合并,适合过滤成员。用 [T] extends [U] 包装后则整体判断;这不是看到 extends 就一定遍历。Strings<T>=T extends string?T:never 将 string|number 变成 string,而整体是否全为 string 得 false。never 在分发中像空联合,Strings<never> 得 never;整体 [never] extends [string] 却成立,若业务要求空类型为 false,需要先独立判 never。

infer 匹配结构,不运行程序

infer 在条件类型匹配中引入待推断部分,例如 T extends readonly (infer U)[] ? U : never 提取数组或只读元组元素联合;提取函数返回类型也只是分析签名,不会执行函数或验证网络字段。复杂工具应覆盖联合、never、unknown、any 与只读数据等反例。类型参数只出现一次或层层递归却最后大量 as,常提示收益不足;过深的类型程序会增加检查耗时与错误理解成本。优先用直接联合、标准工具和有限映射表达业务关系。

最小示例

typescript
typescript
type Route = { path: `/${string}`; method: 'GET' | 'POST' };
const routes = {
  home: { path: '/', method: 'GET' },
  save: { path: '/save', method: 'POST' }
} as const satisfies Record<'home' | 'save', Route>;
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const homePath: '/' = get(routes, 'home').path;
// @ts-expect-error: 不存在该键
get(routes, 'missing');
type Strings<T> = T extends string ? T : never;
type Whole<T> = [T] extends [string] ? true : false;
type A = Strings<string | number>; // string
type B = Whole<string | number>;   // false

预期编译器保留 homePath 的精确类型,并拒绝 missing;@ts-expect-error 应对应真实错误,删掉注释才能直接看到报错。A 和 B 展示逐项过滤与整体判断区别,它们在生成 JavaScript 后不存在。配置对象仍是普通运行时对象,as const 没有执行深冻结,代码也没有检验远端配置。

环境:TypeScript 4.9+;建议 tsc --strict --noEmit 检查。

核验:未执行教学示例;说明中的结果为机制推演,不是本轮测试通过记录。

边界与取舍

边界

  • 满足泛型约束不等于能够构造所有可能的 T;as T 只是压住报错,不能修正契约。
  • 只读视图不消除可变别名,strictFunctionTypes 的方法例外也不保证实际回调安全。
  • satisfies、infer、条件类型均不执行运行时验证;any 与 never 等边界必须单独写类型测试。

取舍

  • 精确泛型增强调用提示和错误定位,但无意义类型参数使 API 难读;保留真实关系即可。
  • 固定键 Record 提供配置完整性,动态键更灵活;选择由是否有封闭键集合决定,不能只追求“类型最严格”。

口述示范

我会把泛型解释成输入输出关系,用 keyof 与索引访问保持键值对应,再说明约束不是任意 T 的构造许可。配置用 satisfies 做检查,回调按读写位置讨论兼容,条件类型明确分发、元组包装和 never 边界;所有这些都停留在编译期。

章节练习

练习 1

为假设的 TranslateFlow 路由配置保留精确路径,同时禁止缺少 save 或多出未知路由。

展开检查点
  • 用固定键 Record 与 as const satisfies;get 返回 T[K],避免把键拓宽成 string。
  • 加入缺项、错方法、错误路径和未知键的编译失败用例,动态数据另行校验。
练习 2

设计只读动物列表和事件回调,再写一个提取字符串联合的工具类型。

展开检查点
  • 证明只处理 Dog 的函数不能处理任意 Animal;只读视图不能阻止其他别名修改。
  • 解释裸 T 分发、元组整体判断、infer 结构匹配,并测试 never 与 any。

参考来源与核验边界

核验日期 2026-10-04。satisfies 需 TS 4.9+,建议 strict 与 noEmit 做正反例检查。本章 TypeScript 示例未执行编译;原题 J24 的历史验证状态继续保持 NOT_RUN,不把建议命令当通过记录。 本章新增示例未在本轮执行,不新增测试通过声明。

内容版本:2026-10-04.2 · 题目和来源 ID 保持原样,本站不将面经标签解释为企业官方出题或高频保证。