TypeScript 类型体操:什么时候值,什么时候在炫技
Twitter 上有个小流派,TypeScript 巫师们发 200 行 conditional type 计算"类型检查期算出 TypeScript 程序的依赖图"。挺帅。但放进生产代码几乎总是错误。
这是一份"什么时候伸手要高级类型、什么时候退回、真实 ROI 长什么样"的指南。
TypeScript 类型四层
第 1 层:基本类型 —— string、number、boolean、命名 interface、unknown。
第 2 层:工具类型 —— Partial、Pick、Omit、Record、Readonly。标准库,可放心用。
第 3 层:条件类型 + mapped 类型 —— T extends U ? X : Y、{ [K in keyof T]: ... }。强大,偶尔必要。
第 4 层:类型层面编程 —— 模板字符串字面量、递归 conditional、模拟算术、类型系统里写解析器。除了库代码,几乎总是错误。
应用代码该住第 1 / 2 层。库代码(工具库、查询构建器、ORM)偶尔有资格上第 3 层。第 4 层让你应该问"我真需要,还是在解谜?"
第 2 层胜利:值回票价的工具类型
type User = { id: string; name: string; email: string; age: number }; type UserUpdate = Partial<User>; // 全可选 type PublicUser = Omit<User, 'email'>; // 隐藏隐私 type UsernameOnly = Pick<User, 'name'>; // 只要名字 type UserMap = Record<string, User>; // 按 ID 字典
这四个工具类型覆盖真实代码 80% 的"类型变换"需求。内置、易懂、senior 5 秒看穿意图。
模式:发现自己写 interface UserUpdate { id?: string; name?: string; ... } 时,换成 type UserUpdate = Partial<User>。代码少,自动跟随 User。
第 3 层:条件类型帮忙的时候
真实例子:给事件发射器加类型。
type Events = { click: { x: number; y: number }; scroll: { delta: number }; open: { source: string }; }; function on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void) { /* ... */ } on('click', (e) => console.log(e.x)); // ✅ 有类型 on('scroll', (e) => console.log(e.delta)); on('open', (e) => console.log(e.x)); // ❌ 不存在 'x'
K extends keyof Events 约束 + Events[K] 索引访问给你每个事件载荷完整 IntelliSense。这是 conditional / mapped 拉真活:手写 on(e: 'click', h: ...) 20 次会痛。
另一个真胜利:discriminated union + 穷举检查。
type Action = { type: 'add'; value: number } | { type: 'remove'; id: string } | { type: 'reset' }; function reduce(action: Action): string { switch (action.type) { case 'add': return `adding ${action.value}`; case 'remove': return `removing ${action.id}`; case 'reset': return 'resetting'; default: const _exhaustive: never = action; // 强制处理所有 case return _exhaustive; } }
Action 加 { type: 'undo' } 时 _exhaustive 行报错,你去补。这是类型驱动重构的最佳形态。
第 4 层:哪里走偏
我在生产代码看过类型像这样:
type CamelCase<S extends string> = S extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize<CamelCase<Tail>>}` : S; type CamelCaseKeys<T> = { [K in keyof T as K extends string ? CamelCase<K> : K]: T[K] };
能跑。类型检查 API 响应"魔法"地给你 camelCase key。它也:
- 把项目增量类型检查翻倍
- 产生不可读的错误信息
- 下一个维护者不可能继承
- 解决一个本可以"用
data.snake_case_field忽略约定不一致"或"加小运行时辅助函数"就解决的问题
要问的问题:这个类型每月省我多于 30 分钟人工时间吗? 不?用运行时类型断言或手写 interface,往前走。
编译期代价是真的
每个条件类型、每个带 K extends 约束的 mapped 类型、每个模板字符串计算 —— 都加到 TypeScript 编译器每次 tsc 和每次编辑器保存的工作量上。
走太远的症状:
- "Type instantiation is excessively deep" 报错
- 基础查找 IntelliSense 延迟 >1 秒
- 即使有缓存增量构建还是慢
- 报错跨 50+ 行 "这个不能赋给那个不能赋给..."
这种时候,修法很少是"优化类型",是"简化类型"。
第 4 层值回票价的两种情况
库代码,类型就是产品。
ORM 像 Drizzle、Prisma、Kysely。状态库如 Zustand 的 middleware 类型。typed 路径的路由库。这些库的价值就是类型层面的魔法 —— 用户拿到 IDE 自动完成,作者付维护成本,几千用户受益。
写这类库就是你的工作描述。用这类库就是不付成本拿好处。
封闭世界字符串空间。
type Route = '/users' | '/users/:id' | '/posts' | '/posts/:slug'; type Params<R extends Route> = R extends `${string}:${infer P}` ? { [K in P]: string } : {};
字符串空间小且已知(路由列表、事件类型、权限集),从字符串模式推类型给真好用。约束作用域,代价可接受。
陷阱:孤立看来很帅的类型
type DeepMerge<A, B> 在类型层面递归对象合并看推特很帅。真实代码里通常你只想要:
function deepMerge<A, B>(a: A, b: B): A & B { /* ... */ }
A & B 交叉"错"(重叠字段产生 never),但实操你在自己控制的对象上调用,能用。100 行 DeepMerge 技术更对,实际没必要。
同样适用于"type-safe Object.entries"、"type-safe SQL query builder"(除非写 ORM)、"从 JSON schema 自动推断表单字段类型"。孤立看帅。真实生活里是维护债。
退回的信号
你在写的类型:
- 超过 30 行
- 用了
infer超过两次 - 需要带深度限制的递归类型参数
- 要
// @ts-expect-error才能往返 - 你 30 秒内向同事解释不清
这是该退一步、写运行时版本、接受一些 as 断言或 unknown 返回也行的信号。
一条启发式
每写一个高级类型前问:6 个月后没我在场,同事能维护这个吗?
不能?就是错的抽象。
TL;DR
- 第 1 层(基本类型)+ 第 2 层(工具类型)放心用。
- 第 3 层(条件、mapped)在替代是重复 overload 噩梦时伸手要。
- 应用代码避免第 4 层(类型层面编程)。它属于库代码,类型是产品的地方。
- 编译期代价真实 —— 要深度限制和
infer递归的类型会拖编辑器。 - 拿不准时写运行时版本,接受几个
as断言。 - 对的测试:同事 6 个月后没你帮助能懂吗?
最花哨的类型未必是对的类型。常常对的类型是让 IDE 能帮你、报错可读的"最笨"的那个。
相关阅读
Kubernetes 资源 requests / limits 实战:不会把生产搞挂的设法
怎么在生产里实际设 Kubernetes CPU 与内存的 requests/limits — QoS 类、CPU 节流、OOM kill、那些害公司钱的差别,以及好使的模式。
Vue 3 vs React 2026:下个项目的诚实对比
2026 年 Vue 3 与 React 的诚实对比 — Composition API vs Hooks、性能、生态、TypeScript 表现,以及真正决定选型的标准。
Rust 所有权深入:借用、生命周期,以及那个真正帮你的编译器
一份务实的 Rust 所有权指南 — move 与 borrow、共享借用 vs 独占借用、生命周期省略、让 borrow checker 错误消失的模式,以及让 Rust 突然通透的心智模型。