TypeScript 的类型系统足够强大,于是不少人写起了"类型体操":能在编译期算阶乘、做字符串替换,看起来很酷。但我们在项目里用到的模式其实很少——标准只有一个:它能不能拦住真实的 bug。这篇列出我们真正在用的几种,以及明确不建议用的地方。
条件类型:把"取里面的类型"变成可复用的工具
条件类型(T extends U ? X : Y)最实用的场景,是把第三方库或异步结构里的类型"剥"出来,避免到处手写重复的泛型:
// 取出数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : never
// 取出函数参数类型(做统一封装时很有用)
type ParamsOf<T> = T extends (...args: infer P) => unknown ? P : never
// 把 Promise 里的类型取出来——不过优先用内置的 Awaited
type ValueOf<T> = T extends Promise<infer U> ? U : T
判断标准很简单:这个类型工具是不是被用在两个以上地方?只为了"写得更聪明"而抽出来的类型,通常半年后没人敢动。
模板字面量类型:让字符串约定变成编译期规则
事件名、缓存 key、路由参数这类"有格式约定的字符串",用模板字面量类型可以让拼错在编译期就报出来。这是我们在前端埋点与接口封装里用得最多的一种:
// 事件名必须是「模块:动作」的组合,拼错立刻报错
type Module = 'user' | 'order' | 'product'
type Action = 'created' | 'updated' | 'deleted'
type AppEvent = `${Module}:${Action}` // "user:created" | ... | "product:deleted"
// 形如 /api/users/123 的路由:数字段写成 number,就不会误传字符串
type UserRoute = `/api/users/${number}`
收益很直接:以前要等埋点数据对不上才发现的问题,现在在编辑器里就红了。
可辨识联合:状态机最省事的写法
页面状态(加载中 / 成功 / 失败 / 空)如果用几个布尔标志拼,最容易出现"加载中同时显示错误"这种自相矛盾的状态。把状态写成互斥的几支,编译器会帮你检查有没有漏分支:
type FetchState<T> =
| { kind: 'loading' }
| { kind: 'error'; message: string }
| { kind: 'empty' }
| { kind: 'ready'; data: T }
function render<T>(state: FetchState<T>) {
switch (state.kind) {
case 'loading': return '加载中…'
case 'error': return state.message
case 'empty': return '暂无数据'
case 'ready': return state.data // 这里能直接访问 data,不需要断言
default: {
const never: never = state // 将来新增状态时,这行会编译报错提醒你补分支
return never
}
}
}
两个性价比最高的关键字
satisfies:既能校验"结构对不对",又保留字面量类型的精确推断。写配置对象最合适——加类型注解会丢掉字面量,不加又失去检查,它两个都要:
const routes = {
home: '/',
news: '/news',
} satisfies Record<string, `/${string}`>
// routes.home 的类型精确到 "/",而不是宽泛的 string
品牌类型(Branded Type):运行时都是 string,但编译期把它们区分开,防止把用户 ID 当订单 ID 传进去。我们只在少量"混用会出事故"的 ID 上用,不为此重写整个模型层:
type UserId = string & { readonly __brand: 'UserId' }
type OrderId = string & { readonly __brand: 'OrderId' }
declare function getOrder(id: OrderId): Promise<unknown>
// getOrder(userId) // 传错类型时编译期就会报错
💡 实践建议
- 优先用内置类型(
Awaited/ReturnType/Parameters),自己写之前先查一遍有没有现成的 - 一个类型工具超过五行、或需要注释解释"它在算什么",就换成直白的重复代码——可读性比少写几行重要
- 类型体操会拖慢编译:复杂的递归类型能让大项目的类型检查慢好几倍,改完看一眼耗时
- 类型是防线不是终点——接口返回、URL 参数、本地存储这些运行时边界仍要做校验
我们的取舍
真实项目里我们常用的是上面这几种:条件类型做类型提取、模板字面量约束字符串格式、可辨识联合管状态、satisfies 管配置。共同的判断标准是"写一次,能拦住未来的一批错误"。其余的类型技巧,学到能看懂别人的代码就够了。
如果你手上有一个类型混乱、报错频繁的 TypeScript 项目,我们在类型治理与渐进重构上做过不少这类活,欢迎联系我们聊聊现状。
