第 8 章

条件类型与 infer:从类型中提取子类型

第8章:条件类型与 infer:从类型中提取子类型

理解条件类型与 infer是掌握 TypeScript 类型系统的关键一步。

本章核心问题:如何在实际项目中正确使用从类型中提取子类型?关键的设计决策和陷阱是什么?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

基本语法:T extends U ? X : Y

条件类型的语法看起来像三元表达式,读起来也类似:"如果 T 能赋值给 U,则结果为 X,否则为 Y"

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">; // true,字符串字面量类型也满足 extends string

注意:这是类型层面的判断,不是运行时代码。extends 在条件类型中的含义是"可赋值性":如果类型 T 的任何值都可以赋值给类型 U,则条件成立。

// 嵌套条件类型:多分支判断
type TypeName<T> =
  T extends string  ? "string"  :
  T extends number  ? "number"  :
  T extends boolean ? "boolean" :
  T extends null    ? "null"    :
  T extends undefined ? "undefined" :
  "object";

type R1 = TypeName<string>;    // "string"
type R2 = TypeName<42>;        // "number"
type R3 = TypeName<boolean>;   // "boolean"
type R4 = TypeName<string[]>;  // "object"

分布式条件类型:联合类型的自动展开

T裸类型参数(不被任何包裹结构修饰),且被用在 T extends U ? X : Y 中时,TypeScript 会自动将联合类型的每个成员单独求值,然后将结果合并。

type IsString<T> = T extends string ? true : false;

// T 是联合类型时,条件类型分布到每个成员
type R = IsString<string | number | boolean>;
// 等价于:IsString<string> | IsString<number> | IsString<boolean>
// 等价于:true | false | false
// 化简为:boolean

这就是 ExcludeExtract 工作原理的基础:

// Exclude<T, U> 的工作原理
type Exclude<T, U> = T extends U ? never : T;

type E = 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"
// 结果:never | never | "c" = "c"

分布式触发条件:


阻止分布:用 [T] extends [U]

有时你不希望联合类型被分布展开,用元组包裹即可阻止:

// 分布式版本:联合类型被拆开求值
type IsStringDistributive<T> = T extends string ? true : false;
type D = IsStringDistributive<string | number>; // boolean (true | false)

// 非分布式版本:整个联合类型作为整体判断
type IsStringNonDistributive<T> = [T] extends [string] ? true : false;
type ND = IsStringNonDistributive<string | number>; // false(整体不满足 extends string)

// 实际用途:判断是否为 never
type IsNever<T> = [T] extends [never] ? true : false;

type N1 = IsNever<never>;  // true
type N2 = IsNever<string>; // false

// 为什么不能用分布式?
type BrokenIsNever<T> = T extends never ? true : false;
type Broken = BrokenIsNever<never>; // never,不是 true!
// 原因:never 联合类型展开后没有成员,条件类型结果为 never

Level 2 · 它是怎么运行的(3-5年经验)

infer 关键字:在条件类型中提取类型

infer 是条件类型的核心能力,允许你在匹配模式时捕获某个位置的类型,并给它一个名字用于结果。

语法:在 extends 的右侧,将某个位置替换为 infer X,X 就是被捕获的类型。

提取函数返回类型

// 手动实现 ReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function greet(name: string): string {
  return `Hello, ${name}`;
}

type GreetReturn = MyReturnType<typeof greet>; // string

// 非函数类型返回 never
type NotFn = MyReturnType<string>; // never

提取 Promise 的值类型

type UnpackPromise<T> = T extends Promise<infer V> ? V : T;

type A = UnpackPromise<Promise<string>>;  // string
type B = UnpackPromise<Promise<number[]>>; // number[]
type C = UnpackPromise<string>;           // string(不是 Promise,原样返回)

提取数组元素类型

type ElementType<T> = T extends (infer E)[] ? E : never;

type N = ElementType<number[]>;       // number
type S = ElementType<string[]>;       // string
type U = ElementType<(string | number)[]>; // string | number
type X = ElementType<string>;         // never(不是数组)

// 更通用的版本,同时处理 ReadonlyArray
type ElementType<T> = T extends readonly (infer E)[] ? E : never;

const arr = [1, 2, 3] as const;
type E = ElementType<typeof arr>; // 1 | 2 | 3(字面量类型联合)

提取函数参数类型

type FunctionArgs<T> = T extends (...args: infer P) => any ? P : never;

function login(username: string, password: string, remember: boolean): void {}

type Args = FunctionArgs<typeof login>;
// [username: string, password: string, remember: boolean]

// 提取第一个参数
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type First = FirstArg<typeof login>; // string

// 提取最后一个参数
type LastArg<T> = T extends (...args: infer P) => any
  ? P extends [...any[], infer L] ? L : never
  : never;
type Last = LastArg<typeof login>; // boolean

逐步构建 Awaited<T>(递归条件类型)

TypeScript 4.5 内置了 Awaited<T>,但自己实现它能帮助理解递归条件类型:

// 版本 1:只处理一层 Promise
type Awaited1<T> = T extends Promise<infer V> ? V : T;

type A1 = Awaited1<Promise<string>>; // string
type A2 = Awaited1<Promise<Promise<string>>>; // Promise<string>(只解一层!)

// 版本 2:递归处理多层嵌套
type Awaited2<T> = T extends Promise<infer V> ? Awaited2<V> : T;

type A3 = Awaited2<Promise<string>>;                 // string
type A4 = Awaited2<Promise<Promise<string>>>;        // string
type A5 = Awaited2<Promise<Promise<Promise<number>>>>; // number

// 版本 3:处理 thenable 对象(完整模拟标准库实现)
type Awaited3<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F, ...args: any[]): any } ?
    F extends (value: infer V, ...args: any[]) => any ?
      Awaited3<V> :
      never :
  T;

递归类型的注意事项:

// TypeScript 编译器对递归深度有限制
// 过深的递归会导致 Type instantiation is excessively deep 错误

// 简单函数组合,递归限制较宽松
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// 处理 JSON 类型的递归定义
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

never 在条件类型中的作用:过滤联合成员

never 是联合类型中的"零元素"——任何联合类型加上 never 等于原来的联合类型。利用这个性质可以精确过滤联合类型:

// 过滤掉字符串中不满足某个条件的成员
type FilterString<T, U extends string> = T extends U ? T : never;

type Colors = "red" | "green" | "blue" | "yellow";
type WarmColors = FilterString<Colors, "red" | "yellow">; // "red" | "yellow"

// 实用工具:只保留对象中值为特定类型的键
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  active: boolean;
}

type StringKeys = KeysOfType<User, string>;  // "name" | "email"
type NumberKeys = KeysOfType<User, number>;  // "id" | "age"

拆解 KeysOfType 的工作过程:

// 第一步:映射类型,值类型满足则保留键名,否则替换为 never
{
  [K in keyof User]: User[K] extends string ? K : never
}
// 等价于:
// {
//   id: never;
//   name: "name";
//   email: "email";
//   age: never;
//   active: never;
// }

// 第二步:[keyof User] 取所有值构成联合类型
// never | "name" | "email" | never | never
// = "name" | "email"

实用工具类型

UnpackPromise<T>

type UnpackPromise<T> = T extends Promise<infer V> ? V : T;

// 用于处理 async 函数返回值
async function fetchUser(id: number) {
  return { id, name: "Alice", email: "[email protected]" };
}

type FetchUserResult = UnpackPromise<ReturnType<typeof fetchUser>>;
// { id: number; name: string; email: string }

// 等价于直接用内置的 Awaited:
type FetchUserResult2 = Awaited<ReturnType<typeof fetchUser>>;

ElementType<T>

type ElementType<T> = T extends readonly (infer E)[] ? E : never;

// 从数组类型提取元素,无需知道数组的具体类型
function processFirst<T extends readonly unknown[]>(arr: T): ElementType<T> {
  return arr[0] as ElementType<T>;
}

const nums = [1, 2, 3] as const;
const first = processFirst(nums); // 类型:1(字面量类型!)

FunctionArgs<T> 在高阶函数中的应用

type FunctionArgs<T> = T extends (...args: infer P) => any ? P : never;
type FunctionReturn<T> = T extends (...args: any[]) => infer R ? R : never;

// 构建 memoize 函数,保留原函数的完整类型签名
function memoize<T extends (...args: any[]) => any>(fn: T): T {
  const cache = new Map<string, FunctionReturn<T>>();
  return ((...args: FunctionArgs<T>) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as T;
}

function expensiveCalc(x: number, y: number): number {
  return x * y * Math.PI;
}

const memoCalc = memoize(expensiveCalc);
memoCalc(2, 3); // 类型:number,与原函数完全一致

Level 3 · 规范怎么定义的(资深)

条件类型的核心机制是分布式求值(distributive evaluation):当 T 是裸类型参数且被传入联合类型时,条件类型会对联合的每个成员单独求值。这就是 Exclude<T, U> 能工作的原因——never 在联合中是零元素,会被自动移除。infer 关键字在条件类型的 extends 右侧引入模式匹配变量,编译器会尝试从实际类型中提取匹配该模式的子类型。递归条件类型(TS 4.1)和尾递归优化(TS 4.5)使得 Awaited<T> 等递归解包类型成为可能,但递归深度仍有约 1000 层的限制。

Level 4 · 边界与陷阱(所有人)

反模式:过深的嵌套条件类型

// 反模式:条件类型嵌套超过 3 层,难以阅读和调试
type ComplexType<T> =
  T extends string
    ? T extends `${string}Id`
      ? T extends `user${string}`
        ? "userId"
        : "otherId"
      : "string"
    : T extends number
      ? T extends 0
        ? "zero"
        : "number"
      : "other";

// 更好的做法:分拆成独立的命名类型
type ExtractIdType<T extends string> =
  T extends `user${string}` ? "userId" : "otherId";

type ExtractStringType<T extends string> =
  T extends `${string}Id` ? ExtractIdType<T> : "string";

type ClassifyType<T> =
  T extends string ? ExtractStringType<T> :
  T extends number ? (T extends 0 ? "zero" : "number") :
  "other";

条件类型的最佳实践:

规则 说明
拆分命名 超过 2 层嵌套就提取为独立类型别名
注释意图 写清楚每个分支的语义
测试类型 type Test = Assert<...> 验证边界情况
知道何时停止 如果用普通泛型能解决,不要用条件类型
// 辅助工具:类型层面的断言,用于测试
type Assert<T extends true> = T;
type Equals<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;

// 验证自定义 Awaited 的行为
type Tests = [
  Assert<Equals<Awaited2<Promise<string>>, string>>,
  Assert<Equals<Awaited2<Promise<Promise<number>>>, number>>,
  Assert<Equals<Awaited2<string>, string>>,
];
// 如果任何一个 Equals 返回 false,这行代码就会报错

总结

特性 语法 用途
基础条件类型 T extends U ? X : Y 类型层面的条件判断
分布式条件类型 T 遇到联合类型时自动展开 过滤联合类型成员
阻止分布 [T] extends [U] 整体判断联合类型
infer 提取返回值 T extends (...) => infer R 推断函数返回类型
infer 提取 Promise 值 T extends Promise<infer V> 解包 Promise
infer 提取数组元素 T extends (infer E)[] 获取元素类型
infer 提取参数 T extends (...args: infer P) => 推断参数元组
递归条件类型 类型别名递归引用自身 处理任意深度嵌套
never 过滤 条件返回 never 以移除联合成员 精确过滤联合类型

下一章:装饰器与元编程——如何在类和方法上附加编译期和运行时的元数据。

本章评分
4.8  / 5  (44 评分)

💬 留言讨论