TypeScript高级类型系统的构建基石
TypeScript条件类型和映射类型是构建复杂类型推导的核心工具。条件类型根据输入类型的结构特征分发到不同类型分支,映射类型遍历类型的键集合并对每个键的值类型做变换。两者结合可以构建出自动推断、类型安全的工具类型,减少手动类型声明,消除类型标注与运行时代码的脱节风险。
类型体操(Type Gymnastics)指利用TypeScript类型系统在编译阶段完成类型推导的能力,不包含任何运行时逻辑。掌握这些技巧可以构建可复用的工具类型,在大型项目中统一类型处理规范。
条件类型的基础语法与分发特性
条件类型的语法为T extends U ? X : Y,当T可赋值给U时结果为X,否则为Y。当T是联合类型时,条件类型会自动分发,对联合中的每个成员分别求值后合并结果:
// 基础条件类型:排除null和undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type T1 = NonNullable<string | null>; // string
type T2 = NonNullable<string | null | number>; // string | number
// 分发特性演示:联合类型逐成员分发
type ToArray<T> = T extends any ? T[] : never;
type T3 = ToArray<string | number>; // string[] | number[]
// 阻止分发:用方括号包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type T4 = ToArrayNonDist<string | number>; // (string | number)[]
分发特性在处理联合类型时会产生截然不同的结果。ToArray<string | number>产出的string[] | number[]表示一个数组要么全是字符串要么全是数字,而ToArrayNonDist的结果(string | number)[]表示数组可以混合字符串和数字。根据业务语义选择是否阻止分发。
infer关键字与类型提取
infer在条件类型的extends子句中声明类型变量,由TypeScript推断具体类型。这是提取函数返回值类型、Promise内部类型、数组元素类型的基础:
// 提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<<T>(x: T) => Promise<T>>; // Promise<unknown>
type R3 = ReturnType<(a: number, b: string) => boolean>; // boolean
// 提取Promise内部值类型(递归解包)
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type P1 = Awaited<Promise<string>>; // string
type P2 = Awaited<Promise<Promise<number>>>; // number
type P3 = Awaited<Promise<string | Promise<number>>>; // string | number
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<[number, boolean]>; // number | boolean
// 提取对象构造函数的实例类型
type InstanceType<T> = T extends new (...args: any[]) => infer I ? I : never;
infer的位置决定了提取的目标。在extends右侧的不同位置放置infer,可以提取函数参数类型、构造函数实例类型、元组位置元素类型等。每个infer变量在条件类型为true的分支中可用。
映射类型与键重映射as子句
映射类型遍历类型的键集合,对每个键进行变换。基本形式为{ [K in keyof T]: NewType }。TypeScript 4.1引入的as子句允许在映射过程中重命名键:
// 将所有属性变为可选
type Partial<T> = { [K in keyof T]?: T[K] };
// 将所有属性变为只读
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 CamelToSnake<S extends string> =
S extends `${infer Head}${infer Tail}`
? `${Head extends Uppercase<Head> ? `_${Lowercase<Head>}` : Head}${CamelToSnake<Tail>}`
: S;
type SnakeCased<T> = {
[K in keyof T as CamelToSnake<K & string>]: T[K]
};
interface UserConfig {
userName: string;
apiKey: string;
maxRetry: number;
}
type Result = SnakeCased<UserConfig>;
// { user_name: string; api_key: string; max_retry: number }
// 过滤键:仅保留string类型的属性值
type StringValues<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
模板字面量类型与路径类型推导
模板字面量类型将字符串字面量联合类型与类型推导结合,适合构建路由参数解析、事件名称约束等场景:
// 路由参数提取
type ExtractRouteParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: S extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type Route1 = ExtractRouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
// 事件监听器类型约束
type EventName<T extends string> = `on${Capitalize<T>}`;
type EventHandler<Events extends string> = {
[K in Events as EventName<K>]?: (payload: any) => void
};
type ButtonEvents = EventHandler<"click" | "hover" | "focus">;
// { onClick?: ...; onHover?: ...; onFocus?: ... }
// 对象get路径类型安全
type Get<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Get<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never;
interface Config {
database: { host: string; port: number };
cache: { ttl: number };
}
type Host = Get<Config, "database.host">; // string
type Port = Get<Config, "database.port">; // number
模板字面量类型配合条件类型和映射类型,可以在编译阶段捕获字符串拼接中的类型错误。在路由框架、ORM查询构建器、配置管理库中广泛应用,避免运行时因拼写错误或路径不存在导致的缺陷。
实战工具类型组合应用
组合上述技巧构建DeepPartial深度可选类型,递归地将对象的所有嵌套属性变为可选:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
interface AppConfig {
server: { port: number; host: string };
database: { url: string; pool: { size: number; timeout: number } };
}
// 仅覆盖部分嵌套属性,深层属性自动可选
const patch: DeepPartial<AppConfig> = {
database: { pool: { size: 10 } } // 合法:其他属性可选
};
// 深度只读
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// 路径类型到值类型的映射
type PathToValue<T, P extends string> =
P extends keyof T ? T[P] : never;
// 联合类型转交叉类型
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends
((k: infer I) => void) ? I : never;
type T5 = UnionToIntersection<{ a: string } | { b: number }>;
// { a: string } & { b: number }
这些工具类型直接放入项目的types工具目录供全局复用。每个类型标注在编译阶段完成校验,不引入运行时开销。使用ts-essentials或type-fest等库可以获得经过充分测试的现成实现,适合不希望从零维护类型工具的项目。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-tiao-jian-lei-xing-yu-ying-she-lei-xing-jin-jie/