文章

条件类型的使用深度解析

条件类型等于类型世界的 if-else:T extends U ? X : Y,所有内置工具类型(Exclude、Extract、ReturnType…)都是它的子句。 不理解分发机制,类型体操就寸步难行。

条件类型的使用深度解析

一句话概括

条件类型 = 类型世界的 if-else——T extends U ? X : Y,一个表达式根据输入类型决定输出类型。所有内置工具类型(Exclude、Extract、ReturnType、NonNullable…)都是它的子句。不理解分发机制,类型体操寸步难行。

核心知识点

1. 基础语法:类型三元

1
2
3
4
5
6
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;  // "yes"(字面量 extends string)
type B = IsString<number>;   // "no"
type C = IsString<string>;   // "yes"
type D = IsString<any>;      // "yes" | "no" ← any 特殊:两边都满足

extends 在条件类型中 = “可赋值性检查”(T 能否赋给 U),不是接口的继承语义。同一个关键字,两套含义。

2. 分发条件类型——最核心也最易踩的坑

触发条件:泛型是裸类型参数 + 传入联合类型 → TS 对每个成员分别判断,结果取联合。

1
2
3
4
5
6
7
8
9
type ToArray<T> = T extends any ? T[] : never;
// 传入 T = string | number:
//   → string extends any ? string[] : never  → string[]
//   → number extends any ? number[] : never  → number[]
//   → 结果:string[] | number[]   ← 不是 (string | number)[]!!

// 阻止分发:把 T 包进元组
type ToArraySafe<T> = [T] extends [any] ? T[] : never;
type R = ToArraySafe<string | number>; // (string | number)[] ✅

面试常考:为什么 Exclude<"a" | "b", "a"> 得到 "b"?——分发:"a" extends "a" → never,"b" extends "a" → "b","c" 不存在,结果 never | "b" = "b"。

3. infer —— 在条件类型中「抓取」类型片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 提取函数返回值 —— 面试手写频率 #1
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 提取 Promise 内层类型(递归解包)
type MyAwaited<T> =
  T extends Promise<infer V>
    ? MyAwaited<V>  // 递归,直到不是 Promise
    : T;

type T1 = MyAwaited<Promise<string>>;           // string
type T2 = MyAwaited<Promise<Promise<number>>>;  // number

// 提取数组元素
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string

4. Exclude / Extract / NonNullable —— 分发 + never 的组合

1
2
3
4
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;
// 本质:用分发遍历每个成员,不合要求的返回 never(被 TS 自动消除)

5. 条件类型 + 映射类型 + as = 属性级过滤

1
2
3
4
5
6
7
8
9
10
11
// 只保留函数类型的属性 —— 常用于提取组件方法
type FunctionKeys<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
// { a: "a"; b: never; c: "c" }[keyof T] → "a" | never | "c" → "a" | "c"

// 直接用 as 过滤,更直观
type PickFunctions<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};
// as never 的键被 TS 自动从结果类型中移除

其实你每天都在用

  1. Exclude<keyof T, "id">:泛型组件里排除不需要用户传的 prop
  2. ReturnType<typeof myFn>:第三方库没有导出类型时提取函数返回值
  3. NonNullable<Props["onChange"]>:提取可选 prop 的”必填版”
  4. Awaited<ReturnType<typeof fetchUser>>:去掉 Promise 层拿到真实 data 类型
  5. React 的 ComponentProps<typeof Button>:未经导出的组件 props 类型靠这招提取

常见误解(FAQ)

❌ 误区 1:「条件类型的分发总是会发生」

只有裸类型参数才触发分发。T extends ... 分发;[T] extends ... 不分发;{ k: T } extends ... 也不分发。写 IsUnion<T> 这种工具类型时,是否分发直接决定结果对错。

❌ 误区 2:「T extends never 传入 never 返回 true」

不!never 是空联合类型,分发后没有任何成员,整个表达式结果为 never。正确检测 never 的写法:[T] extends [never] ? true : false。这是类型体操入门级陷阱。

❌ 误区 3:「infer 可以在任何地方声明类型变量」

只能在条件类型的 extends 子句的真分支(? 之后)中使用。type X = infer R 是语法错误。它必须依附于 extends 模式匹配。

❌ 误区 4:「infer 只是 ReturnType 那种一次性提取」

infer + 递归 = 图灵完备的类型计算。Awaited<T> 递归解包 Promise、KebabCase<S> 递归处理字符串——infer 配合递归可以做字符串解析、深度类型变换这些”类型编程”。

一句话总结

T extends U 是类型的模式匹配,infer 是匹配到的捕获组,分发是联合类型的自动遍历——三个概念合在一起,就是 TypeScript 从”类型标注”到”类型编程”的完整范式。

本文由作者按照 CC BY 4.0 进行授权