首页 / TypeScript 入门教程 / 条件类型

TypeScript 入门教程

条件类型

本教程共 80 篇 · 第 43 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程条件类型泛型

本节目标:掌握 T extends U ? X : Y 的语法和语义,理解分布条件类型的行为,学会用 never 做类型过滤,并为下一章映射类型与条件类型的组合打好基础。

上一章你已经用到了条件类型——ExcludeExtract 的核心实现就是一行条件类型。这一章我们系统地把这个语法吃透。

条件类型是 TypeScript 类型系统中的”if/else”——它让你根据类型的结构关系做分支判断,在类型层面实现逻辑。

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

语法跟 JavaScript 的三元表达式一模一样,只是把值换成了类型:

type IsString<T> = T extends string ? "是字符串" : "不是字符串";

type A = IsString<"hello">; // "是字符串"
type B = IsString<42>;      // "不是字符串"
type C = IsString<string>;  // "是字符串"

T extends U 的意思是”T 是否可以赋值给 U”。如果可以,走 ? 后面的分支;如果不可以,走 : 后面的分支。

光这么用没什么意思

问题来了:上面这种写法,你自己看一眼就知道结果,干嘛用条件类型?条件类型的威力在于和泛型组合——当 T 还没确定的时候,条件类型才能发挥”动态判断”的能力:

type Flatten<T> = T extends any[] ? T[number] : T;

type Str = Flatten<string[]>;  // string(从数组里提取元素类型)
type Num = Flatten<number>;    // number(不是数组,原样返回)
type Bool = Flatten<boolean[]>; // boolean

Flatten<T> 等价于:如果 T 是数组,返回它的元素类型;否则原样返回。这里的 T[number] 是索引访问类型——它取数组的元素类型。string[]string[] [number] 就是 string

你不需要提前知道 T 是什么,条件类型让类型系统在”调用处”根据传入的实际类型做判断。

取代函数重载的真实场景

条件类型可以大幅简化需要用重载做到的逻辑。假设你要根据传入类型的不同返回不同标签:

不用条件类型你得这样:

interface IdLabel { id: number }
interface NameLabel { name: string }

// 三个重载!每加一种新类型,重载数量翻倍
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "未实现";
}

用条件类型可以精简到一个函数:

type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;

function createLabel<T extends number | string>(input: T): NameOrId<T> {
  throw "未实现";
}

const a = createLabel("typescript"); // NameLabel
const b = createLabel(100);           // IdLabel

分布条件类型:对联合类型的”映射”

前面 Exclude 的实现你已经看到了——T extends U ? never : T。当 T 是联合类型时,它会把条件分配到联合的每个成员上。这就是分布条件类型(distributive conditional types)

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

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

发生了什么?ToArray<string | number> 被展开为:

ToArray<string> | ToArray<number>
→ (string extends any ? string[] : never) | (number extends any ? number[] : never)
→ string[] | number[]

这个行为对联合类型来说几乎总是你想要的——你要的正是”把联合里每种类型分别处理,然后把结果合并”。

为什么分布条件总是跟泛型搭配

注意一个关键细节:条件类型只有在作用于裸类型参数(naked type parameter)时才会分布

// ✅ 会分布——T 是裸类型参数
type Distributive<T> = T extends any ? T[] : never;
type D = Distributive<string | number>; // string[] | number[]

// ❌ 不会分布——T 被包在了数组里
type NonDistributive<T> = T[] extends any[] ? T[] : never;
type ND = NonDistributive<string | number>; // (string | number)[]

第二个例子中,T[] 不是裸类型参数——条件类型先计算 T[](结果是 (string | number)[]),再判断 (string | number)[] extends any[],不会触发分布。

如何禁用分布

如果你不想让条件类型分布(比如你要的就是 (string | number)[] 而不是 string[] | number[]),用方括号包裹类型参数:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result = ToArrayNonDist<string | number>;
// Result = (string | number)[]    —— 不分布

[T] extends [any]T 包在元组里,破坏了裸类型参数的条件,分布行为就不会触发。

never 过滤技巧

联合类型中 never 会被自动吸收——这意味着你可以用 never 来”去除”不想要的类型。这就是 Exclude 的原理:

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

但这个技巧不只限于 Exclude。你可以在任何条件类型中用 never 做过滤:

// 只保留字符串字面量类型
type StringOnly<T> = T extends string ? T : never;

type Filtered = StringOnly<"a" | "b" | 1 | 2>;
// "a" | "b"    —— 1 和 2 被过滤掉了

在后面的映射类型中,你还会看到这个技巧——它可以在遍历对象类型的时候”跳过”不满足条件的属性。

条件类型约束:在 true 分支里收窄类型

条件类型的 true 分支不仅返回结果,还会**收窄(narrow)**泛型参数的类型。这意味着在 true 分支内部,TypeScript 知道 T 满足 extends 后面的条件:

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

interface Email {
  message: string;
  sender: string;
}

interface Notification {
  message: number;
  level: string;
}

type A = MessageOf<Email>;         // string
type B = MessageOf<Notification>;  // number
type C = MessageOf<{ x: number }>; // never(没有 message 属性)

? 后面的分支(true 分支),TypeScript 知道 T 一定有 message 属性,所以 T["message"] 是合法的。

如果不用条件类型,你得这样写:

// ❌ 报错:T 上不存在 message
// type MessageOf<T> = T["message"];

// ✅ 但必须约束 T——不灵活
type MessageOf2<T extends { message: unknown }> = T["message"];

第二种写法的问题在于:T 被”永久约束”了——你不能对没有 message 属性的类型使用。条件类型把约束变成了”按需判断”:有就取,没有就返回 never

条件类型 + infer:预告

条件类型最强大的部分其实是 infer——上一章 ReturnTypeParameters 里已经用到了。这里做一个预告,为后续章节铺路。

infer 让你在条件类型的模式匹配中”捕捉”类型片段:

// 不用 infer:用 T[number] 手动提取数组元素
type Flatten1<T> = T extends any[] ? T[number] : T;

// 用 infer:让 TypeScript 推断数组元素类型
type Flatten2<T> = T extends Array<infer Item> ? Item : T;

当你在 extends 后面用了 infer Item,TypeScript 会自动推断那个位置的具体类型并绑定到 Item。对于复杂的类型模式(比如提取 Promise 内层类型、函数的参数元组等),infer 比手动拆解直观得多。

下一章讲映射类型时,你会看到条件类型 + 映射类型如何组合出强大的类型变换能力——比如”把一个类型的所有函数属性挑出来”或者”把所有属性名转成 getter 方法名”。

小结

条件类型是 TypeScript 类型系统的”运行时判断”——只是这个”运行时”发生在编译期。三个核心要点:

  1. 语法T extends U ? X : Y,跟 JavaScript 三元表达式同款。
  2. 分布:作用于裸类型参数时,自动分配给联合类型的每个成员。
  3. 过滤:利用 never 在联合中会被吸收的特性,实现”剔除不想要的类型”。

加上 infer,条件类型就成了 TypeScript 类型体操的核心舞台。不过在实际开发中,你不需要写太多复杂的条件类型——前面两章的内置工具类型已经覆盖了绝大部分日常需求。