TypeScript条件类型与映射类型进阶:类型体操实战技巧

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/

(0)
小编小编
上一篇 2小时前
下一篇 2小时前

相关推荐