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 能帮你、报错可读的"最笨"的那个。
我们是一支开发者小团队,负责构建和维护 ToolsForge —— 150+ 个完全在浏览器本地运行、隐私优先的工具。这些文章来自我们日常的工程实践,而不是纸上谈兵。
了解我们相关阅读
一张照片能做多少事:AI 改图工具实战指南
换发型、换发色、换装、转卡通、风格迁移、室内设计、老照片修复——用一张照片走完这些 AI 改图流程,以及怎么让结果更稳、更像本人。
把脸保住:AI 改图提示词怎么写才不"换人"
换发型、换装、换风格时,AI 老把脸也改了?这篇讲清楚改图提示词的写法:保人约束、双通道(gpt-image-2 / Nano Banana)各自的脾气,以及一套能直接抄的模板。
Kubernetes 资源 requests / limits 实战:不会把生产搞挂的设法
怎么在生产里实际设 Kubernetes CPU 与内存的 requests/limits — QoS 类、CPU 节流、OOM kill、那些害公司钱的差别,以及好使的模式。