文章目录

TypeScript 条件类型:T extends U ? X : Y

发布于 2026-04-02 20:11:12 · 浏览 6 次 · 评论 0 条

TypeScript 条件类型:T extends U ? X : Y

TypeScript 的条件类型提供了一种根据类型关系动态选择结果类型的机制。其基本语法为 T extends U ? X : Y,含义是:如果类型 T 可以赋值给类型 U(即 TU 的子类型),则整个表达式的结果类型为 X;否则为 Y。这种能力在构建泛型工具类型、实现类型推导和约束时极为强大。


理解基本行为

创建一个最简单的条件类型示例:

type IsString<T> = T extends string ? true : false;
  • 当你使用 IsString<"hello"> 时,由于 "hello"string 的字面量子类型,结果为 true
  • 当你使用 IsString<number> 时,number 不能赋值给 string,结果为 false

注意:这里的 extends 不是指类继承,而是指“可赋值性”(assignability)。只要 T 的值能安全地赋给 U 类型的变量,就满足 T extends U


分布式条件类型

当条件类型的左侧(即 T)是一个泛型类型参数,且该参数被传入一个联合类型(如 string | number)时,TypeScript 会自动将条件类型“分发”到联合类型的每个成员上。

例如:

type ToPrimitive<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : never;

type Test = ToPrimitive<string | number | boolean | Date>;
// 结果为 string | number | boolean

执行过程如下

  1. string | number | boolean | Date 拆分为四个独立类型。
  2. 对每个类型分别应用 ToPrimitive
    • stringstring
    • numbernumber
    • booleanboolean
    • Datenever
  3. 合并结果:string | number | boolean | never,而 never 在联合类型中会被自动忽略,最终得到 string | number | boolean

要禁用分布行为,可将泛型参数包裹在元组中:

type NotDistributive<T> = [T] extends [string] ? true : false;
type A = NotDistributive<string | number>; // false(整体判断,非分发)

实用场景:提取函数返回类型

利用条件类型配合 infer 关键字,可以提取复杂类型中的信息。例如,获取函数的返回类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
  • 如果 T 是一个函数类型,则通过 infer R 捕获其返回类型 R
  • 否则返回 never

使用

function add(a: number, b: number): number {
  return a + b;
}

type AddResult = ReturnType<typeof add>; // number

构建过滤工具类型

编写一个从联合类型中剔除某些类型的工具:

type Exclude<T, U> = T extends U ? never : T;
  • T 中的每个成员,若其可赋值给 U,则替换为 never;否则保留。
  • 联合类型中的 never 会被自动剔除。

调用

type T0 = Exclude<'a' | 'b' | 'c', 'a' | 'c'>; // 'b'
type T1 = Exclude<string | number | (() => void), Function>; // string | number

TypeScript 内置的 Exclude 工具类型正是基于此原理。

相反,提取属于某类的类型:

type Extract<T, U> = T extends U ? T : never;

示例

type T2 = Extract<'a' | 'b' | 1 | 2, string>; // 'a' | 'b'

处理嵌套结构:递归条件类型

条件类型支持递归,可用于处理嵌套对象或数组。

定义一个将所有属性转为只读的深层工具类型:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends Record<string, unknown>
    ? DeepReadonly<T[K]>
    : T[K];
};
  • 遍历 T 的每个键 K
  • 如果 T[K] 是一个对象(通过 Record<string, unknown> 判断),则递归应用 DeepReadonly
  • 否则保持原类型。

使用

interface User {
  name: string;
  address: {
    city: string;
    zip: number;
  };
}

type ReadonlyUser = DeepReadonly<User>;
// name: string(只读)
// address: { readonly city: string; readonly zip: number; }(深层只读)

常见陷阱与注意事项

  1. 空对象类型陷阱
    Record<string, unknown> 匹配几乎所有对象,但不包括 nullundefined。若需兼容,应使用更宽松的判断:

    type IsObject<T> = T extends object ? (T extends null ? never : T) : never;
  2. any 与 unknown 的行为差异

    • any extends string ? X : Y 会同时走 XY 分支(因为 any 既是任何类型的子类型,又可以接受任何类型),最终结果为 X | Y
    • unknown extends string ? X : Y 结果为 Y,因为 unknown 不能赋值给 string
  3. 避免无限递归
    在递归条件类型中,确保有明确的终止条件。例如,对原始类型(string, number, boolean 等)不再递归。


高级技巧:结合映射类型与条件类型

组合映射类型和条件类型,可实现按需转换属性。

例如,仅将可选属性变为必选,其余不变:

type RequiredIfOptional<T> = {
  [K in keyof T]-?: T[K] extends Required<T>[K] ? T[K] : T[K];
};

更实用的例子:将对象中所有函数属性的返回类型提取出来:

type FunctionReturnTypes<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => infer R ? R : never;
}[keyof T];

解释

  • 先构造一个新对象,每个属性值是原函数的返回类型(非函数则为 never)。
  • 然后通过 [keyof T] 索引访问,得到所有返回类型的联合。

测试

interface API {
  getUser(): User;
  save(data: string): boolean;
  version: string;
}

type Returns = FunctionReturnTypes<API>; // User | boolean

内置工具类型的实现参考

TypeScript 官方提供的许多工具类型都依赖条件类型。以下是部分实现逻辑:

工具类型 简化实现
Exclude<T, U> T extends U ? never : T
Extract<T, U> T extends U ? T : never
NonNullable<T> T extends null \| undefined ? never : T
Parameters<T> T extends (...args: infer P) => any ? P : never
ConstructorParameters<T> T extends new (...args: infer P) => any ? P : never

这些类型展示了条件类型与 infer 的典型结合方式。


掌握 T extends U ? X : Y 的核心在于理解“可赋值性”和“分布行为”。通过组合 infer、映射类型和递归,你可以构建出高度灵活的类型逻辑,让 TypeScript 在编译期为你完成复杂的类型推导和验证。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文