TypeScript 专题|类型体操 |
satisfies是 TS 4.9 引入的无声英雄,不用显式类型注解,却能让类型推导「既宽松又严格」------前端老鸟的生产秘籍。
问题场景:显式类型注解的两个痛点
你有没有遇到过这样的两难?
typescript
// 场景 A:定义配置对象,用了 Record,但 IDE 提示全丢了
const routes: Record<string, { path: string; component: string }> = {
home: { path: '/', component: 'HomePage' }, // ✅ 类型检查通过
about: { path: '/about', component: 'AboutPage' },
}
routes.home // IDE 只能告诉你这是 `{ path: string; component: string }`
// 却不知道 routes 上有 home、about 两个具体 key ❌
// 场景 B:不用类型注解,推导保留但类型检查没了
const routes = {
home: { path: '/', component: 'HomePage' },
about: { path: '/about', component: 'AboutPage' },
admin: { path: '/admin', component: 'AdminPage', extra: true }, // 🤫 没人发现多了字段
}
就想要这样: 既要「宽松到能保留推导的具体类型」,又要「严格到能拦截多余的字段」------satisfies 就是为此而生。
原因分析:为什么 Record<string, T> 不够好?
TS 的类型系统存在一个古老的矛盾:
| 目标 | Record<string, T> |
不加注解 |
|---|---|---|
| 约束值符合 T | ✅ 严格检查 | ❌ 随便写 |
| 保留键的具体类型 | ❌ 全变成 string index | ✅ IDE 完美提示 |
| 禁止多余字段 | ✅ 隐式 any 报错 | ❌ 静默通过 |
Record<string, T> 的副作用:它把对象 拍平 成了索引签名,丢掉了你花心思命名的每个 key 的类型信息。
satisfies 的妙处:不改变变量的类型 ,只是让 TS 验证右侧表达式是否满足某个类型约束。
解决方案:satisfies 的 5 个实战场景
场景 1:配置对象------保留 key 推导 + 约束 value 结构
typescript
interface RouteConfig {
path: string
component: string
meta?: { title: string; requiresAuth?: boolean }
}
const routes = {
home: { path: '/', component: 'HomePage', meta: { title: '首页' } },
about: { path: '/about', component: 'AboutPage' },
users: { path: '/users/:id', component: 'UserDetail', meta: { title: '用户详情', requiresAuth: true } },
// admin: { path: '/admin', component: 'AdminPage', extra: true }, // 🛑 Error: 对象字面量只能指定已知属性
} satisfies Record<string, RouteConfig>
// IDE 完美推导:
routes.home // ✅ 类型是 { path: string; component: string; meta?: { title: string; requiresAuth?: boolean } }
routes.about // ✅ 不推断多余的 undefined
场景 2:事件映射------类型安全的 emit 调用
typescript
interface EventMap {
click: { x: number; y: number }
search: { query: string }
logout: void
}
const handlers = {
click: (e: { x: number; y: number }) => console.log(e.x, e.y),
search: (q: { query: string }) => fetch(`/api?q=${q.query}`),
logout: () => fetch('/api/logout'),
// extra: () => {}, // 🛑 Error: 不在 EventMap 中
} satisfies { [K in keyof EventMap]: (payload: EventMap[K]) => void }
// 调用时类型完全保留
handlers.click({ x: 10, y: 20 }) // ✅ 类型正确
handlers.search({ query: 'vue' }) // ✅ 类型正确
场景 3:枚举替代方案------字符串联合 + 映射对象
typescript
// 不用 enum,不用 namespace,纯字符串联合
type Color = 'red' | 'green' | 'blue'
const colorValues = {
red: '#ff0000',
green: '#00ff00',
blue: '#0000ff',
// yellow: '#ffff00', // 🛑 Error: 'yellow' 不在 Color 中
} satisfies Record<Color, string>
// 同时得到:
colorValues.red // 类型是 '#ff0000' 字面量(不是 string!)
colorValues.blue // 类型是 '#0000ff'
// ✨ 还能自动提示所有颜色
场景 4:多态函数返回------联合类型推导更精确
typescript
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number }
// 传统做法:显式类型注解会丢失具体分支类型
function createShape(kind: Shape['kind']): Shape {
// ❌ return 后面你只能写通用的 Shape 结构
return null!
}
// 用 satisfies 做内部映射表:
const shapeFactories = {
circle: (r: number): Shape => ({ kind: 'circle', radius: r }),
rect: (w: number, h: number): Shape => ({ kind: 'rect', width: w, height: h }),
triangle: (b: number, h: number): Shape => ({ kind: 'triangle', base: b, height: h }),
} satisfies Record<string, (...args: number[]) => Shape>
// 调用时 factory 类型保留:
shapeFactories.circle(5) // ✅ 明确返回 circle 类型
场景 5:API 响应类型守卫------运行时验证 + 编译时约束
typescript
interface ApiUser {
id: number
name: string
email: string
}
// 运行时校验函数
const validateUser = (data: unknown): data is ApiUser => {
return typeof data === 'object' && data !== null
&& 'id' in data && typeof (data as any).id === 'number'
&& 'name' in data && typeof (data as any).name === 'string'
}
// 校验函数集合------用 satisfies 确保每个函数签名一致
const validators = {
user: validateUser,
// 如果再加一个新的校验函数,必须同样返回 data is XXX
// profile: (data: unknown): data is { bio: string } => ..., // ✅ 符合签名
} satisfies Record<string, (data: unknown) => boolean>
// ✓ 编译时:每个 validators 成员签名被统一检查
// ✓ 运行时:validators.user(data) 保留类型守卫效果
要点总结
- 核心理解 :
satisfies是 验证 而非 注解------不改变表达式类型,只约束它「必须满足 T」 - 保留推导 :比
Record<string, T>更好,因为不会丢失具体 key 的类型信息 - 常见误区 :不要写成
foo satisfies T = bar,正确语法是const foo = bar satisfies T - 最佳拍档 :
satisfies+ 映射类型{ [K in keyof T]: ... }威力翻倍 - 版本要求:TypeScript ≥ 4.9,如果你还用 4.8 及以下,这是升级的理由 😉
一句话总结 :
satisfies让 TS 从「管你写什么,反正我说了算」变成「你写你的,我帮你兜底」------二者兼顾,才是真正的类型安全。