模板字面量类型
模板字面量类型基础
TypeScript 4.1 引入模板字面量类型,允许通过反引号语法定义字符串字面量类型的集合。与联合类型结合时,模板会展开所有可能的组合。
- 字面量约束:精确约束字符串只能是特定格式(如 HTTP 方法、事件名称)
- 字符串操作:支持 uppercase、lowercase、capitalize、uncapitalize 内置工具
- 模式提取:通过 infer 在模板内提取字符串的特定部分
// 基础模板字面量类型 type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; // 模板字面量与联合类型展开 type EventName = 'click' | 'focus' | 'blur'; type HandlerName = 'on-'.Capitalize<EventName>; // HandlerName = 'on-Click' | 'on-Focus' | 'on-Blur' // 实际应用:类型安全的 CSS 类名构造 type Size = 'sm' | 'md' | 'lg'; type Variant = 'primary' | 'secondary'; type ClassName = 'btn-'.Capitalize<Size> | 'btn-'.Capitalize<Variant>; // ClassName = 'btn-Sm' | 'btn-Md' | 'btn-Lg' | 'btn-Primary' | 'btn-Secondary' // 更复杂的场景:提取路径参数 type Route = '/users/:id' | '/products/:category/:id'; // 提取路径参数名称 type ExtractParams<T extends string> = T extends `${string}:${infer P}${string}` ? ExtractParams<DropFirst<T>> | P : never; type UserParams = ExtractParams<'/users/:id'>; // UserParams = 'id' type ProductParams = ExtractParams<'/products/:category/:id'>; // ProductParams = 'category' | 'id'
// 实际工程应用:类型安全的 API 路径构建 type ApiPaths = { '/users': { GET: UserListResponse; POST: CreateUserRequest }; '/users/:id': { GET: UserResponse; PUT: UpdateUserRequest; DELETE: void }; '/orders/:orderId/items': { GET: OrderItemsResponse }; }; type ExtractPathParams<T extends string> = T extends `${string}:${infer P}(${string}` extends `${infer _}:${infer Rest}` ? ExtractPathParams<`${string}:${Rest}`> : T extends `${string}:${infer P}` ? P : never; type PathParams<T extends keyof ApiPaths> = ExtractPathParams<T>; // 用例 type UserIdParam = PathParams<'/users/:id'>; // UserIdParam = 'id'
IDE 自动补全
模板字面量类型在 IDE 中提供精确的智能提示。当定义 on${Event} 类型时,输入 on- 后会自动提示所有可能的 Event 组合。
大小写变换
内置的 Uppercase、Lowercase、Capitalize、Uncapitalize 可组合使用,实现命名规范的标准化转换。
模式匹配
模板字面量的 ${string} 占位符可与 infer 结合,实现复杂字符串结构的模式识别与提取。
类型守卫
类型守卫的分类
类型守卫是 TypeScript 进行类型收窄的核心机制。它允许在条件分支中缩小联合类型的范围,使开发者能够在运行时安全地访问特定类型的属性和方法。
- typeof 守卫:用于基础类型(string、number、boolean、bigint、symbol、undefined、function)
- instanceof 守卫:用于类实例的类型检查
- 自定义守卫:返回 is 谓词的函数,精确控制类型收窄逻辑
- in 守卫:检查对象属性是否存在
// 自定义类型守卫 interface Cat { meow(): void; } interface Dog { bark(): void; } function isCat(animal: Cat | Dog): animal is Cat { return (animal as Cat).meow !== undefined; } function speak(animal: Cat | Dog) { if (isCat(animal)) { animal.meow(); // TypeScript 知道这是 Cat,meow() 可访问 } else { animal.bark(); // TypeScript 知道这是 Dog,bark() 可访问 } } // in 守卫 function processValue(value: { kind: 'success'; data: string } | { kind: 'error'; message: string }) { if ('data' in value) { console.log(value.data); // success 分支 } else { console.log(value.message); // error 分支 } } // 可辨识联合(Discriminated Union)—— 最推荐的模式 type Result<T, E extends Error> = | { status: 'fulfilled'; value: T } | { status: 'rejected'; reason: E }; function handleResult<T, E extends Error>(result: Result<T, E>) { switch (result.status) { case 'fulfilled': return result.value; // T 类型 case 'rejected': throw result.reason; // E 类型 } }
typeof 的局限
typeof 仅能识别 JS 原始类型和 function,无法区分具体对象类型或类层次。当需要区分两个不同的 interface 时,typeof 无能为力,需要自定义守卫或可辨识联合。
never 收窄
类型守卫配合 exhaustive check(穷尽性检查)可以确保所有联合成员都被处理。如果遗漏分支,TypeScript 会将剩余类型收窄为 never,触发编译错误。
可辨识联合(Discriminated Union)是 TypeScript 中最可靠的结构化类型检查模式。为每个联合成员添加唯一的 discriminant 字段(如 kind/status/type),使类型收窄在任意条件下都可预测。
条件类型分发
条件类型的分发机制
当条件类型(T extends U ? X : Y)中的 T 是联合类型时,TypeScript 会将条件分发到联合的每个成员,分别计算后将结果合并为新的联合类型。这是对条件类型最强大也最容易被误解的特性。
- 分发触发条件:T 必须是裸类型(未包裹在任何泛型中)。
[T] extends语法可抑制分发 - 分发方向:联合的每个元素分别代入条件,计算结果后合并
- 空联合:never 作为泛型时,条件类型直接返回 never(因为 never 无任何可分配类型)
// 分发示例:联合类型的展开 type ToArray<T> = T extends any ? T[] : never; type StrOrNumArr = ToArray<'a' | number>; // 展开过程: ToArray<'a'> | ToArray= 'a'[] | number[] // 最终结果: ('a'[] | number[]) —— 这是联合,不是交叉 // 抑制分发:[T] extends 而不是 T extends type IsNever<T> = [T] extends [never] ? true : false; type CheckString = IsNever<'string'>; // false —— 不分发 type CheckNever = IsNever<never>; // true —— never 特殊处理 // 实际应用:类型过滤(Exclude / Extract 原理) type MyExclude<T, U> = T extends U ? never : T; type Filtered = MyExclude<'a' | 'b' | 'c', 'b'>; // 展开: ('a' extends 'b' ? never : 'a') | ('b' extends 'b' ? never : 'b') | ('c' extends 'b' ? never : 'c') // 结果: 'a' | never | 'c' = 'a' | 'c' // 嵌套分发:更复杂的场景 type Flatten<T> = T extends Array<infer U> ? U : T; type Nested = (string[] | number[])[]; type FlatResult = Flatten<Nested>; // 分发后: Flatten<string[]> | Flatten<number[]> = string | number
分发边界
条件类型在裸泛型时触发分发。如果 T 被 Array<T> 或 [T] 包裹,分发会被抑制。这在实现 IsAny、IsNever 等工具类型时至关重要。
分发与交叉
联合类型的分发会产生联合结果;交叉类型(&)则不会分发,结果是各分支的交叉。理解这个差异是高级类型编程的基础。
分发与 any
any 与任何类型 extends 时,由于 any 的特殊性,分发结果为所有可能的联合。慎用 any 作为条件类型的输入。
infer 推导
infer 的工作机制
infer 只能在条件类型的 extends 子句中使用,用于声明一个局部类型变量,让 TypeScript 在模式匹配时自动推断并捕获该位置的类型。它是实现高级工具类型(如 ReturnType、Parameters)的基础机制。
- 只能在 extends 子句中使用:infer 是模式匹配的一部分,不能独立存在
- 延迟推断:当条件成立时,infer 声明的类型变量才会被具体化
- 多 infer 联合:可以在同一个模式中使用多个 infer 声明
// 经典工具类型:ReturnType 实现 type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never; type Fn = (name: string, age: number) => Promise<{ id: string }>; type FnReturn = ReturnType<Fn>; // FnReturn = Promise<{ id: string }> // Parameters:提取函数参数类型 type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; // 递归 infer:提取嵌套类型 type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> // 递归处理嵌套 Promise : T; type DeepResult = DeepAwaited<Promise<Promise<Promise<string>>>>; // DeepResult = string // 提取数组元素类型(递归) type DeepExtract<T> = T extends Array<infer U> ? DeepExtract<U> : T; type Extracted = DeepExtract<string[][][]>; // Extracted = string // 构造函数类型推断 type ConstructorParams<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never; class User { constructor(name: string, age: number) {} } type UserCtorParams = ConstructorParams<typeof User>; // UserCtorParams = [name: string, age: number]
infer 在模板字面量中的使用
模板字面量类型支持 infer,可在字符串的特定位置提取子串。结合 Uppercase 等内置工具,可实现类型安全的字符串解析。
${infer Prefix}static${infer _}:提取static前面的部分get${infer Name}:提取 get 后面的名称字面量
infer 与分布式条件类型
当 infer 遇到联合类型时,会对每个联合成员分别推断,然后将结果联合起来。如果希望抑制分布式,应使用 [T] extends 而非裸 T。
infer 的本质是模式匹配中的"捕获变量"。当 extends 左侧的模式与右侧类型匹配时,infer 声明的位置会捕获对应的子类型。这是 TypeScript 类型系统实现递归计算的核心机制。
映射类型修饰符
映射类型(Mapped Types)
映射类型是 TypeScript 类型系统中最强大的工具之一,它通过 { [K in keyof T]: ... } 语法遍历一个类型的所有键,并逐个进行转换。加上修饰符前缀可以精细控制每个属性的可读性和可选性。
- readonly:控制属性是否可写。
-readonly移除只读,+readonly添加只读 - ?:控制属性是否可选。
-?移除可选(变为必需),+?添加可选 - as 重新映射(TS 4.1+):重新映射键名,实现键的过滤与变换
// 基础映射类型:给所有属性添加只读 type Readonly<T> = { readonly [K in keyof T]: T[K]; }; // 移除只读(-readonly) type Mutable<T> = { -readonly [K in keyof T]: T[K]; }; // 移除可选(-?)—— 所有属性变为必需 type Required<T> = { [K in keyof T -?]: T[K]; }; // 添加可选(+?)—— 所有属性变为可选 type Partial<T> = { [K in keyof T +?]: T[K]; }; // 键名重新映射(as 子句) type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; interface User { name: string; age: number; } type UserGetters = Getters<User>; // = { getName: () => string; getAge: () => number } // 键过滤:仅保留 string 类型的属性 type StringOnly<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; // 条件映射:仅处理特定类型的属性 type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; };
同态映射
映射类型作用于某个类型时,如果映射表达式直接引用原类型属性(T[K]),则该映射是同态的——它会保留原始类型的结构特性(如只读、可选)。
异态映射
不直接引用 T[K] 的映射是异态的,如 { [K in keyof T]: SomeOtherType }。异态映射会打破原类型的 readonly 和可选特性。
键名重映射
TS 4.1 的 as 子句允许重新映射键名,可结合条件类型实现键的过滤(never)和变换(模板字面量)。
映射类型配合条件类型和 infer,可以实现类型级的深度计算。如 DeepPartial(深层可选)、DeepMutable(深层可变)、UnionToIntersection(联合转交叉)等。这些工具类型是类型安全框架的核心支柱。