Skip to content
高级类型

高级类型

本章涵盖 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,这些成员就会被自动消除。这就是 ExcludeNonNullable 等内置工具类型的底层机制。


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  // 非标准用法仅作演示
    : never

infer 可用于的位置

位置 示例
数组/元组元素 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 或先在条件类型中过滤