TypeScript 的类型系统是图灵完备的。这意味着理论上你可以在类型层面完成任何计算。了解这些高级特性,不仅能看懂开源库的类型定义,还能为自己的代码写出精准的类型推导。
核心工具:条件类型 + infer
如果只能掌握一个特性,那必须是条件类型 + infer。它类似类型层面的三元表达式:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
}
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
}
type PathOf<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: K extends string
?
| `${Prefix}${K}`
| PathOf<T[K], `${Prefix}${K}.`>
: never
}[keyof T & string]
: never
type ValueAtPath<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? ValueAtPath<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never
type CamelCase<S extends string> = S extends `${infer First}_${infer Rest}`
? `${First}${Capitalize<CamelCase<Rest>>}`
: S
type SnakeCase<S extends string> = S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `_${Lowercase<First>}${SnakeCase<Rest>}`
: `${First}${SnakeCase<Rest>}`
: S
type Length<T extends readonly any[]> = T['length']
type First<T extends readonly any[]> = T extends [infer F, ...any] ? F : never
type Last<T extends readonly any[]> = T extends [...any, infer L] ? L : never
type TupleToUnion<T extends readonly any[]> = T[number]
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
type IsNever<T> = [T] extends [never] ? true : false
type IsUnion<T, U = T> = IsNever<T> extends true
? false
: T extends U
? [U] extends [T]
? false
: true
: false
type GetReturnType<F> = F extends (...args: any) => infer R ? R : never
type GetParameters<F> = F extends (...args: infer P) => any ? P : never
常见工具类型复杂度分级
| 难度 | 类型 | 核心技巧 | 实战用途 |
|---|---|---|---|
| 入门 | Partial/Required/Readonly | 映射类型 + ?/-?/readonly | 每日必用 |
| 入门 | Pick/Omit/Record/Extract/Exclude | as 子句 + keyof + extends | 每日必用 |
| 中级 | ReturnType/Parameters/InstanceType | infer 函数签名 | 类型提取 |
| 中级 | DeepPartial/DeepReadonly | 递归 + 条件类型 | 配置对象 |
| 中级 | CamelCase/SnakeCase | 模板字面量 + 递归 | API 转换层 |
| 高级 | PathOf/ValueAtPath | 递归 + 模板字面量 | lodash.get 类型安全 |
| 高级 | UnionToIntersection | 逆变体位置 infer | 理论题 |
| 地狱 | ParseQueryString/Format | 多变量递归 | 类型竞技 |
分布式条件类型的秘密
当条件类型左边是裸类型参数(naked type parameter)且传入联合类型时,TypeScript 会自动分发:
type Wrap<T> = T extends any ? { value: T } : never
type X = Wrap<string | number>
// 等于 { value: string } | { value: number }
// 因为 string 和 number 分别代入再联合
这是 90% 的人不理解但 90% 的工具类型在用的特性。记住:想让联合"拆开来处理"就用裸泛型,想让联合"整体处理"就用元组包裹 [T] extends [U]。
模板字面量类型
TS 4.1 引入的模板字面量让字符串处理进入了类型层面:
type CSSVar<K extends string> = `--${K}`
type V = CSSVar<'primary-color'> // '--primary-color'
type Concat<A extends string, B extends string> = `${A}${B}`
最佳实践
- 先写出 JS 运行时版本,再"翻译"成类型版本,能极大降低思考难度
- 善用 type-challenges 仓库,做 20 道中级题就能熟练大部分套路
- 不要在业务代码中滥用,类型体操过深会让同事看不懂;库代码再深挖
- 给复杂类型写测试用例,用
Expect<Equal<X, Y>>范式防止回归 - 超过 5 层嵌套时拆分子类型,和正常代码一样要抽象分层
总结
类型体操不是炫技。它的本质是:把原本需要运行时才能发现的错误,提前到编译期;把原本需要手写的类型声明,交给编译器自动推导。在正确的地方(库开发、框架封装、通用工具)使用它,能极大提升代码质量和开发体验。而在业务代码中,保持克制,够用就好。