TypeScript 类型体操与工程实践
🧩 类型体操的目标不是追求复杂,而是用可维护的类型约束表达真实业务规则,并把错误尽可能提前到编译阶段。
类型系统基础
- 联合类型
A | B:值满足其中一种类型。 - 交叉类型
A & B:同时具备多种类型的结构。 - 泛型:让类型参数随调用方传入,保留输入和输出之间的关系。
- 类型收窄:通过
typeof、in、判别字段或自定义守卫缩小联合类型。
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: Error }
function unwrap<T>(result: Result<T>): T {
if (result.ok) return result.data
throw result.error
}
常用类型工具
type User = {
id: string
name: string
email?: string
}
type UserPatch = Partial<Omit<User, 'id'>>
type PublicUser = Pick<User, 'id' | 'name'>
type UserMap = Record<string, User>
内置工具的核心通常来自映射类型、条件类型与索引访问类型。
条件类型与 infer
type AwaitedValue<T> = T extends Promise<infer U> ? AwaitedValue<U> : T
type FnReturn<T> = T extends (...args: any[]) => infer R ? R : never
条件类型遇到裸类型参数的联合类型时会分发:
type ToArray<T> = T extends unknown ? T[] : never
type Value = ToArray<string | number> // string[] | number[]
若要阻止分发,可使用元组包装:[T] extends [U]。
映射类型与键重映射
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type ReadonlyDeep<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: ReadonlyDeep<T[K]> }
: T
模板字面量类型
适合表达事件名、路由参数和样式 token:
type EventName<T extends string> = `${T}Changed`
type CSSSize = `${number}px` | `${number}rem` | 0
工程实践
1. 边界处验证运行时数据
类型只存在于编译期。接口响应、URL 参数、LocalStorage 等外部输入必须使用 Zod、Valibot 或手写守卫校验。
import { z } from 'zod'
const UserSchema = z.object({ id: z.string(), name: z.string() })
type User = z.infer<typeof UserSchema>
const user = UserSchema.parse(input)
2. 优先使用 unknown
any 会关闭类型检查;unknown 要求先收窄再使用。
3. 使用 satisfies 保留推导
const routes = {
home: '/',
profile: '/users/:id',
} satisfies Record<string, string>
4. 用判别联合建模状态
避免 loading?: boolean、data?: T、error?: Error 产生非法组合。
5. 控制类型复杂度
- 为复杂工具类型写测试和注释。
- 避免无限递归和过深实例化。
- 公共 API 显式声明返回类型。
- 业务类型优先清晰,不为减少几行代码而牺牲可读性。
tsconfig 建议
- 开启
strict - 开启
noUncheckedIndexedAccess - 开启
exactOptionalPropertyTypes - 库项目生成声明文件并谨慎维护导出边界
自检清单
- 能使用泛型保留输入输出关系
- 能解释条件类型分发
- 能用判别联合消除非法状态
- 能区分编译期类型与运行时校验
- 能读懂常见映射类型和
infer