高级类型编程

掌握条件类型、映射类型、模板字面量类型与 infer 关键字,具备设计复杂类型工具的能力。

高级类型编程

概述

TypeScript 的高级类型编程是一套图灵完备的类型级计算系统。条件类型提供分支逻辑,映射类型提供遍历能力,模板字面量类型处理字符串级运算,infer 实现模式匹配。掌握这些工具,可以构建精确的类型约束,将更多错误从运行时提前到编译时。

面试定位:高级类型题是区分中级与高级 TypeScript 开发者的关键。面试官通过 type-challenges 风格的问题,考察候选人对类型系统的深度理解和类型级编程能力。


条件类型(Conditional Types)

基础语法

条件类型的语法类似三元表达式,在类型层面执行条件判断:

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"

分布式条件类型(Distributive Conditional Types)

当条件类型作用于裸类型参数(naked type parameter)的联合类型时,会自动分布到每个联合成员上:

type ToArray<T> = T extends unknown ? T[] : never;

type Result = ToArray<string | number>;
// string[] | number[](而非 (string | number)[])

要阻止分布行为,用方括号包裹类型参数:

type ToArray<T> = [T] extends [unknown] ? T[] : never;

type Result = ToArray<string | number>;
// (string | number)[]

条件类型与联合类型过滤

// 提取函数类型
type FunctionKeys<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

interface API {
  name: string;
  getUser: (id: string) => User;
  updateUser: (user: User) => void;
}

type Methods = FunctionKeys<API>; // "getUser" | "updateUser"

infer 关键字

infer 在条件类型中引入待推断的类型变量,实现类型级的模式匹配:

基础用法

// 推断函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 推断数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type Item = ElementType<string[]>; // string

// 推断 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type Result = UnwrapPromise<Promise<Promise<string>>>; // string

多位置 infer

infer 出现在多个位置时,TypeScript 会尝试统一推断:

// 推断第一个和最后一个元素
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type F = First<[1, 2, 3]>; // 1
type L = Last<[1, 2, 3]>;  // 3

infer 与约束(TypeScript 4.7+)

// infer 位置添加 extends 约束
type FirstString<T> = T extends [infer S extends string, ...any[]] ? S : never;

type A = FirstString<["hello", 42]>; // "hello"
type B = FirstString<[42, "hello"]>; // never

映射类型(Mapped Types)

基础语法

映射类型遍历联合类型的每个成员,生成新的属性:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Optional<T> = {
  [P in keyof T]?: T[P];
};

键重映射(Key Remapping,TypeScript 4.1+)

as 子句允许在映射过程中转换键:

// 为每个属性生成 getter
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

过滤键

通过 as 子句中返回 never 来过滤属性:

// 只保留字符串值的属性
type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  email: string;
}

type OnlyStrings = StringProps<Mixed>;
// { name: string; email: string }

修饰符操作

+- 前缀可以添加或移除 readonly? 修饰符:

// 移除 readonly
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// 移除可选
type Required<T> = {
  [P in keyof T]-?: T[P];
};

模板字面量类型(Template Literal Types)

TypeScript 4.1+ 引入了类型级的模板字符串:

基础用法

type EventName = `on${Capitalize<"click" | "focus" | "blur">}`;
// "onClick" | "onFocus" | "onBlur"

type Locale = "en" | "zh" | "ja";
type Currency = "USD" | "CNY" | "JPY";
type LocalizedPrice = `${Locale}_${Currency}`;
// "en_USD" | "en_CNY" | "en_JPY" | "zh_USD" | ... (9种组合)

模式匹配与 infer

// 解析路由参数
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

实际应用:类型安全的 CSS 属性

type CSSValue = `${number}${"px" | "em" | "rem" | "%"}`;

function setWidth(el: HTMLElement, width: CSSValue) {
  el.style.width = width;
}

setWidth(el, "100px");  // ✅
setWidth(el, "2em");    // ✅
setWidth(el, "100");    // ❌ 缺少单位

递归类型

TypeScript 4.1+ 对递归类型有良好支持:

// 深层 Readonly
type DeepReadonly<T> = T extends (...args: any[]) => any
  ? T
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

// JSON 类型
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// 路径类型
type Path<T, K extends keyof T = keyof T> =
  K extends string
    ? T[K] extends object
      ? K | `${K}.${Path<T[K]>}`
      : K
    : never;

interface Config {
  db: { host: string; port: number };
  app: { name: string };
}

type ConfigPath = Path<Config>;
// "db" | "app" | "db.host" | "db.port" | "app.name"

注意:递归深度有限制(通常约 50 层)。过深的递归会触发 Type instantiation is excessively deep and possibly infinite 错误。


实战:类型体操常见模式

元组转联合

type TupleToUnion<T extends readonly any[]> = T[number];
type Union = TupleToUnion<[1, 2, 3]>; // 1 | 2 | 3

联合转交叉

type UnionToIntersection<U> =
  (U extends unknown ? (k: U) => void : never) extends
  (k: infer I) => void ? I : never;

type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }

获取对象深层值类型

type Get<T, K extends string> =
  K extends `${infer First}.${infer Rest}`
    ? First extends keyof T
      ? Get<T[First], Rest>
      : never
    : K extends keyof T
      ? T[K]
      : never;

type Value = Get<{ a: { b: { c: number } } }, "a.b.c">; // number

类型编程的工程边界

高级类型编程强大但有代价:

  1. 编译性能:复杂的条件类型和递归类型显著增加编译时间
  2. 可读性:过度复杂的类型让团队成员难以理解和维护
  3. 错误信息:深层泛型的类型错误信息往往冗长而难以定位

实用原则

  • 能用简单类型表达的,不使用复杂类型编程
  • 类型工具应有清晰的文档和测试用例
  • 递归深度控制在合理范围内
  • 考虑使用 // @ts-expect-error 或类型断言作为"逃生舱"

面试高频问题

Q: 什么是分布式条件类型?如何控制其行为?

回答要点:当条件类型的被检查类型是裸类型参数(直接使用 T 而非 [T]),且传入联合类型时,条件判断会分布到每个联合成员上独立执行,最终结果合并为联合类型。用方括号 [T] extends [U] 包裹可以阻止分布行为。典型应用是 Exclude<T, U>Extract<T, U> 的实现。

Q: 请解释 infer 关键字的作用

回答要点infer 在条件类型的 extends 子句中声明一个待推断的类型变量,让 TypeScript 从匹配的结构中提取类型。它类似于正则表达式中的捕获组——先描述结构模式,再从中提取需要的部分。ReturnType<T>Parameters<T> 等内置工具类型的核心就是 infer


延展阅读