本节摘要:类型级编程把类型当作函数的输入与输出——类型检查器在编译期对"类型"执行映射、条件、递归等运算,算出的结果用于接纳或拒绝代码。本节用 TypeScript 的条件类型与映射类型做主标本,对照 C++ 类型萃取,展示这门技术如何把成片的运行期断言替换为零成本的编译期检查。
普通函数吃值、吐值;类型级"函数"吃类型、吐类型。Array<string> 可以看作数组类型构造器对 string 的调用,Promise<User[]> 是两次调用的叠加。当语言允许你编写这个层面的自定义运算——输入任意类型,按规则产出新类型——类型系统就从"标注工具"升格为"可编程的证明器"。
它与 3.5 编译期计算是姊妹技术,分工恰好互补:编译期计算对值运算(算出 120 这个数),类型级编程对类型运算(算出"这个函数返回什么类型")。两者共同的哲学是把错误挡在运行之前。
TypeScript 的条件类型语法直接借自三目表达式:
// 语义:T 能赋值给 U 吗?能就取 X 类型,否则取 Y 类型 type IsString<T> = T extends string ? "yes" : "no"; type A = IsString<"hello">; // "yes" type B = IsString<42>; // "no" // 分布式条件类型:对联合类型自动逐成员展开 type ToArray<T> = T extends any ? T[] : never; type C = ToArray<string | number>; // string[] | number[](不是 数组的数组)
操作→结果:A 与 B 不是运行结果,是类型检查器算出的类型级常量。解读:第三例的分布行为最值得咀嚼——联合类型会被自动拆开逐个代入再合并。这个看似古怪的规则恰恰是类型级编程的主力武器:批量变换。
条件类型是分支,映射类型是循环。工程中最常用的场景是"基于既有类型派生变体":
interface ApiConfig { host: string; port: number; retries: number; } // 把每个属性都变成只读 type Frozen<T> = { readonly [K in keyof T]: T[K] }; // 把每个属性变成"取值函数"的字典 —— 配置对象的访问器版 type Accessors<T> = { [K in keyof T]: () => T[K] }; type ConfigGetters = Accessors<ApiConfig>; // 等价于 { host: () => string; port: () => number; retries: () => number } // 加条件过滤:只挑字符串属性 type StringKeys<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T]; type HostOnly = StringKeys<ApiConfig>; // "host"
操作→结果:keyof T 取出属性名联合,in 逐名遍历,方括号内完成逐名变换——三个部件合成一台"类型加工机"。解读:Accessors 这类派生在旧时代要靠手写或代码生成,现在类型检查器即时算出,且永远与源类型同步:ApiConfig 加一个字段,ConfigGetters 自动跟上。同步性正是类型级编程对 4.5 生成管线的降维优势——它根本不存在"产物过期"问题。
一次真实重构展示收益形状。原代码里,事件总线用字符串登记事件名,监听器参数全靠 any 与运行期断言把关:
// 重构前:拼错事件名、传错参数,都要等运行期炸 bus.on("user-login", (payload: any) => { ... }); // 重构后:事件名与载荷类型的映射表交给类型系统 interface EventMap { "user-login": { userId: string; at: number }; "order-paid": { orderId: string; amount: number }; } bus.on("user-login", (p) => p.userId.length); // p 自动推导,写错字段立即报错 bus.on("user-logn", () => {}); // 编译错误:事件名不存在
解读:改造没有引入任何新运行时代码——on 的签名挂上 EventMap 后,检查器自动完成"名字合法、载荷类型匹配"两重证明。原来散布在业务里的运行期断言被整体删除,这就是类型级编程的收益形状:不是加功能,是删检查。变式:把映射表做成泛型工具类型(从路由表推导参数类型、从数据库 schema 推导行类型),就是"全栈类型安全"类框架的核心原理。
C++ 的类型萃取(type traits)是同一思想的另一方言:std::is_integral<T>::value 在编译期回答"是不是整数",std::enable_if 按条件启用重载——语法更涩,语义与 TypeScript 的条件类型同构。C++20 Concepts(4.2 已实操)本质是把这套惯用法扶正成了语言级语法。
问:类型级编程写得越深越好吗? 反了。类型级运算的可读性衰减极快:一层映射人人能懂,三层条件套递归就只有作者能维护——这正是 6.5 要讲的"模板天书"在类型维度的翻版。实践标尺:类型运算的复杂度预算按"团队平均水平能读懂"设定,超出部分回退到显式代码或代码生成。
问:类型级编程与 3.5 的编译期计算是什么关系? 同一哲学的两个维度:编译期计算对"值"运算(算出常量 120),类型级编程对"类型"运算(算出派生类型)。两者可以协作——值参数驱动类型选择、类型参数约束值范围——静态语言正沿着这个方向把"编译期"整体变成一台可编程机器。理解了这层对称,新语言发布时你只需要问一句:它在值与类型两个维度上,各开放了多少编译期可编程性?
keyof 加 in 合成类型加工机,派生结果与源类型永远同步。类型系统管住了"代码对不对"。下一节处理另一类顽疾:日志、事务、权限这些谁都要、谁都不属于业务的横切逻辑。