高级类型
本章涵盖 TypeScript 类型系统中最为强大的部分:映射类型、条件类型、infer、模板字面量类型、satisfies 运算符以及递归类型。它们是构建类型安全库和复杂泛型的基石。
映射类型(Mapped Types)
映射类型让你基于已有类型的 key 生成新的类型——迭代每个属性并对其类型或修饰符进行变换。
基本语法
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type Partial<T> = {
[P in keyof T]?: T[P]
}
// 使用
interface User {
name: string
age: number
}
type ReadonlyUser = Readonly<User>
// { readonly name: string; readonly age: number }
🔬 深入原理:
[P in keyof T]迭代T的所有 key(类似 JavaScript 的for...in),为每个 key 生成一个新的属性。P是迭代变量,代表当前的属性名,T[P]则提取该属性的类型。这就是"映射"的含义——将一种类型的属性一一映射为另一种。
属性修饰符
通过 + / - 前缀控制 readonly 和 ? 修饰符的增删:
// 移除 readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}
// 移除可选(变为必填)
type Required<T> = {
[P in keyof T]-?: T[P]
}
// 同时添加(+ 是默认,可省略)
type ReadonlyPartial<T> = {
+readonly [P in keyof T]+?: T[P]
}| 前缀 | 含义 |
|---|---|
+readonly |
添加 readonly(默认行为) |
-readonly |
移除 readonly |
+? |
添加可选(默认行为) |
-? |
移除可选,变为必填 |
Key 重映射(as 子句)— TS 4.1+
通过 as 子句可以重命名或过滤映射类型的 key:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
}
interface Person { name: string; age: number }
type PersonGetters = Getters<Person>
// { getName: () => string; getAge: () => number }
利用 never 过滤 key:
// 排除特定 key
type ExcludeKey<T, K> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
interface Todo {
id: number
title: string
createdAt: Date
}
type TodoWithoutDate = ExcludeKey<Todo, "createdAt">
// { id: number; title: string }
// 只保留字符串类型的值属性
type PickStringProps<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P]
}
type StringProps = PickStringProps<{ a: string; b: number; c: string }>
// { a: string; c: string }
条件类型(Conditional Types)
基本语法
type IsString<T> = T extends string ? true : false
type A = IsString<"hello"> // true
type B = IsString<number> // false
type C = IsString<string> // true
语法如同三元运算符,但作用于类型层面。extends 在这里表示"可赋值给"或"是……的子类型"。
分布式条件类型
当 T 是联合类型时,T extends U ? X : Y 会自动分布到联合类型的每个成员上:
type ToArray<T> = T extends unknown ? T[] : never
type Result = ToArray<string | number>
// = (string extends unknown ? string[] : never)
// | (number extends unknown ? number[] : never)
// = string[] | number[]
//
// ❌ 注意:不是 (string | number)[]
// 内置工具类型的实现原理
type Exclude<T, U> = T extends U ? never : T
type Result2 = Exclude<"a" | "b" | "c", "a" | "b">
// ("a" extends "a"|"b" ? never : "a") → never
// | ("b" extends "a"|"b" ? never : "b") → never
// | ("c" extends "a"|"b" ? never : "c") → "c"
// = "c"
🚨 陷阱:分布式行为只在
T是裸类型参数(bare type parameter)时触发。用[T]包裹(即[T] extends [U]),则关闭分布式行为,类型会以整体形式参与判断。
type NonDistributive<T> = [T] extends [unknown] ? T[] : never
type Result3 = NonDistributive<string | number>
// (string | number)[] 而不是 string[] | number[]
never 的过滤行为
type NonNullable<T> = T extends null | undefined ? never : T
type Clean = NonNullable<string | null | undefined>
// string | null | undefined
// → string | never | never
// → string(never 在联合类型中会被自动消去)
🔬 深入原理:
never是联合类型的"零元"——A | never恒等于A。条件类型正是利用这个特性来过滤类型:让需要排除的成员返回never,这些成员就会被自动消除。这就是Exclude、NonNullable等内置工具类型的底层机制。
infer 关键字
infer 让你在条件类型的 extends 子句中声明并捕获一个类型变量,从而从已有类型中"提取"出子类型。
基本用法
// 提取数组元素类型
type ElementOf<T> = T extends (infer U)[] ? U : never
type E1 = ElementOf<string[]> // string
type E2 = ElementOf<number[]> // number
// 提取函数返回值类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = MyReturnType<() => string> // string
type R2 = MyReturnType<(x: number) => void> // void
// 提取 Promise 内层类型(递归)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type A1 = Awaited<Promise<string>> // string
type A2 = Awaited<Promise<Promise<number>>> // number(递归解开两层)
在同一条件中使用多个 infer
// 交换函数的参数与返回值类型
type SwapFn<T> = T extends (...args: infer A) => infer R
? (...args: R[]) => A
: never
type F1 = (x: number, y: string) => boolean
type F2 = SwapFn<F1>
// (args: boolean[]) => [x: number, y: string]
// 提取 Promise 的 fulfilled 值和 rejected 值
type PromiseLike<T> = T extends PromiseLike<infer V>
? V extends infer V ? V : never // 非标准用法仅作演示
: neverinfer 可用于的位置
| 位置 | 示例 |
|---|---|
| 数组/元组元素 | T extends (infer U)[] |
| 函数参数 | T extends (...args: infer P) => any |
| 函数返回值 | T extends (...args: any[]) => infer R |
| 构造函数参数 | T extends new (...args: infer P) => any |
| 构造函数实例 | T extends new (...args: any[]) => infer I |
| 模板字面量 | T extends `${infer Prefix}/${infer Suffix}` |
| Promise 值 | T extends Promise<infer V> |
// 从模板字面量中提取路径段
type ExtractPath<T> = T extends `${infer Segment}/${infer Rest}`
? Segment | ExtractPath<Rest>
: T
type Segments = ExtractPath<"api/v1/users">
// "api" | "v1" | "users"
模板字面量类型(Template Literal Types)— TS 4.1+
基本插值
type Greeting = `Hello, ${string}!`
let g1: Greeting = "Hello, World!" // ✅
let g2: Greeting = "Hello, Alice!" // ✅
type EventName = `on${Capitalize<string>}`
let e1: EventName = "onClick" // ✅
let e2: EventName = "onFocus" // ✅
与联合类型的组合(笛卡尔积)
当插值中的类型是联合类型时,会生成所有可能的组合:
type Color = "red" | "green" | "blue"
type Size = "sm" | "md" | "lg"
type ColorSize = `${Color}-${Size}`
// "red-sm" | "red-md" | "red-lg"
// | "green-sm" | "green-md" | "green-lg"
// | "blue-sm" | "blue-md" | "blue-lg"
// 实用示例:生成 CSS 类名的联合类型
type Align = "start" | "center" | "end"
type Axis = "x" | "y"
type JustifyClass = `justify-${Align}` // "justify-start" | "justify-center" | "justify-end"
type ItemsClass = `items-${Align}` // "items-start" | "items-center" | "items-end"
type DirectionClass = `flex-${"row" | "col"}${"" | "-reverse"}`
// "flex-row" | "flex-row-reverse" | "flex-col" | "flex-col-reverse"
⚡ 性能提示:大联合类型的模板字面量组合会产生 N x M 个可能值。例如 50 个颜色 x 50 个尺寸 = 2500 个字面量类型。这可能导致编辑器卡顿和类型检查变慢。控制联合类型成员在合理范围内,或拆分为多层计算。
字符串操作类型(Intrinsic String Manipulation Types)
TypeScript 内置四种字符串操作类型,仅在类型层面对字符串字面量进行操作:
| 类型 | 输入 | 输出 |
|---|---|---|
Uppercase<S> |
"hello" |
"HELLO" |
Lowercase<S> |
"HELLO" |
"hello" |
Capitalize<S> |
"hello" |
"Hello" |
Uncapitalize<S> |
"Hello" |
"hello" |
// 组合使用:将 camelCase 转为事件处理器名称
type ToHandler<T extends string> = `on${Capitalize<T>}`
type ClickHandler = ToHandler<"click"> // "onClick"
// 将对象 key 转为 setter 方法名
type Setters<T> = {
[P in keyof T & string as `set${Capitalize<P>}`]: (value: T[P]) => void
}satisfies 运算符 — TS 4.9
satisfies 让你在验证表达式满足某个类型的同时,保留其更精确的推断类型。
基本对比
type ColorRGB = { r: number; g: number; b: number }
// ❌ 只用类型注解:palette 的类型被拓宽为 Record<string, ColorRGB>
// 后续访问 palette.red 只会得到 ColorRGB,且 blue 的拼写错误不会被发现
const palette1: Record<string, ColorRGB> = {
red: { r: 255, g: 0, b: 0 },
blue: [0, 0, 255], // ❌ 编译错误!数组不满足 ColorRGB
}
// ✅ 用 satisfies:既能检查类型,又保留字面量键名和更精确的类型
const palette2 = {
red: { r: 255, g: 0, b: 0 },
blue: { r: 0, g: 0, b: 255 },
} satisfies Record<string, ColorRGB>
// palette2.red 的类型是 { r: number; g: number; b: number }
// palette2.blue 同样
// 且 "blue" 这个 key 被保留,palette2["blue"] 可正常访问
💡 最佳实践:
satisfies是: Type注解和as断言之间的"第三条路":它像注解一样检查类型兼容性,但像断言一样保留字面量类型。当你既要约束又要精确性时,首选satisfies。
保留字面量类型
const config = {
api: "https://api.example.com",
timeout: 5000,
retries: 3,
} satisfies Record<string, string | number>
// config.api 类型是 "https://api.example.com"(字面量),而不是宽泛的 string
// config.timeout 类型是 5000,不是 number
// 这使得后续使用时可以获得精确的类型提示
典型使用场景
// 场景1:约束对象值但不丢失 key 信息
type Routes = Record<string, { path: string; auth: boolean }>
const routes = {
home: { path: "/", auth: false },
admin: { path: "/admin", auth: true },
profile: { path: "/profile/:id", auth: true },
} satisfies Routes
// routes.home.path 的类型是 "/"(字面量),而不是 string
// 场景2:确保对象符合联合类型的某一种
type Color = string | { r: number; g: number; b: number }
const red: Color = { r: 255, g: 0, b: 0 }
// red.r ❌ 错误:Color 可能是 string,没有 .r 属性
const green = { r: 0, g: 255, b: 0 } satisfies Color
// green.r ✅ 类型是 number!TypeScript 知道 green 是对象形式
递归类型 — TS 4.1+
TypeScript 4.1 开始支持在类型别名中递归引用自身,使得定义自引用数据结构成为可能。
递归数据定义
// JSON 类型(经典递归定义)
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue }
// 测试
const json: JSONValue = {
name: "Alice",
age: 30,
hobbies: ["reading", { name: "coding", level: 5 }], // 多层嵌套 ✅
}// 树形结构
type TreeNode<T> = {
value: T
children: TreeNode<T>[]
}递归映射类型
// DeepReadonly:递归地将所有嵌套属性标记为 readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P] // 跳过函数类型
: DeepReadonly<T[P]>
: T[P]
}
interface Nested {
user: { name: string; profile: { avatar: string } }
tags: string[]
}
type ReadonlyNested = DeepReadonly<Nested>
// user、user.name、user.profile、user.profile.avatar 全部 readonly
// DeepPartial:递归地将所有属性变为可选
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Function ? T[P] : DeepPartial<T[P]>
: T[P]
}递归条件类型
// 递归解开多层 Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type Result = Awaited<Promise<Promise<string>>>
// 第一次:Promise<Promise<string>> extends Promise<infer U> → U = Promise<string>
// → Awaited<Promise<string>>
// 第二次:Promise<string> extends Promise<infer U> → U = string
// → Awaited<string>
// 第三次:string extends Promise<infer U> → false
// → string ✅
🚨 陷阱:递归类型不能无限递归。TypeScript 编译器对类型别名的递归深度有隐式限制(通常约 50 层)。超深的嵌套结构或设计不当的递归类型(如缺少终止条件的条件类型)可能导致编辑器卡顿、类型检查超时,甚至产生
Type instantiation is excessively deep and possibly infinite错误。
递归深度控制技巧
// ❌ 可能无限递归:没有终止条件
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T
// ✅ 增加深度计数器(通过元组长度)
type FlattenDepth<
T,
Depth extends number = 5,
Counter extends unknown[] = []
> = Counter["length"] extends Depth
? T
: T extends (infer U)[]
? FlattenDepth<U, Depth, [...Counter, unknown]>
: T实战示例
类型安全的事件系统
综合运用映射类型、模板字面量类型和条件类型,构建一个完全类型安全的事件处理器映射:
// 定义事件及其载荷
type EventMap = {
click: { x: number; y: number }
change: { value: string }
focus: void
blur: void
}
// 自动生成事件处理器类型:click → onClick, change → onChange
type EventHandler<T extends EventMap> = {
[K in keyof T & string as `on${Capitalize<K>}`]: (
data: T[K] extends void ? void : T[K]
) => void
}
// 使用
type Handlers = EventHandler<EventMap>
// {
// onClick: (data: { x: number; y: number }) => void
// onChange: (data: { value: string }) => void
// onFocus: (data: void) => void
// onBlur: (data: void) => void
// }
深度部分更新工具类型
// 允许对嵌套对象进行任意深度的部分更新
type DeepPartial<T> = T extends object
? T extends Function
? T
: { [P in keyof T]?: DeepPartial<T[P]> }
: T
type NestedUser = { name: string; profile: { age: number; address: { city: string } } }
type PartialUser = DeepPartial<NestedUser>
const update: PartialUser = {
profile: {
address: { city: "Beijing" } // ✅ 可以只更新最深层的字段
}
}条件 + 模板字面量:类型安全的 URL 构建器
type BaseURL = "https://api.example.com"
type Endpoint = "/users" | "/posts" | "/comments"
type IDParam = `/${number}`
type FullURL = `${BaseURL}${Endpoint}${IDParam}`
// "https://api.example.com/users/123" | "https://api.example.com/posts/456" | ...
// 提取 URL 中的 ID
type ExtractID<T extends string> = T extends `${string}/${infer ID extends number}` ? ID : never
type ID1 = ExtractID<"/users/42"> // 42
常见陷阱汇总
| 陷阱 | 原因 | 避免方式 |
|---|---|---|
| 分布式条件类型"展开"了联合类型,得到的结果不是预期的整体类型 | 裸类型参数在 extends 中自动分布 |
用 [T] extends [U] 关闭分布式行为 |
satisfies 后的值仍然可能隐式包含多余属性(不触发 excess property check) |
satisfies 不执行 excess property checking |
对于严格的对象形态匹配,使用 : Type 注解代替 |
| 模板字面量联合类型爆炸导致编辑器卡顿 | 多个大联合类型的笛卡尔积组合数量巨大 | 限制联合类型成员数;拆分为多层计算而非一次性组合 |
递归类型缺少终止条件导致 Type instantiation is excessively deep |
类型递归深度超过编译器隐式上限(~50 层) | 确保递归有条件终止;使用深度计数器控制层级 |
不理解 never 在条件类型中的过滤行为,导致联合类型中元素意外消失 |
never 作为联合类型成员时会被自动移除 |
理解 never 是联合类型的"零元";如需保留,用元组 [T] 包裹 |
在 as 子句中忘记用 & string 约束 keyof T(因为 keyof T 可能是 string | number | symbol) |
Capitalize 等方法仅接受 string 类型 |
始终使用 keyof T & string 或先在条件类型中过滤 |