TypeScript 条件类型与 infer — 类型层面的 if/else 与正则捕获,一次讲透
昨天你把 17 个官方工具类型用了一遍,心里一定有个疑问:
Exclude<T, U>凭什么能从联合里"删人"?ReturnType<T>凭什么能"看穿"函数的返回值?答案是两个今天要学的东西:条件类型(类型层面的三元运算)和 infer(类型层面的捕获变量)。学完今天,官方工具类型的源码在你眼里将不再神秘——因为你能亲手写出它们。
目录
一、为什么需要条件类型
1.1 值的世界有 if,类型的世界呢?
// 值层面:根据条件选不同的值
const label = temp > 80 ? "危险" : "正常";
// 类型层面的问题:一个泛型函数,想根据传入类型"决定"返回类型
// 场景:写一个 deepClone,参数是普通对象返回克隆对象,是数组返回数组……
// 没有条件类型之前,你只能写"既定类型":
function clone<T>(value: T): T {
return value; // 返回类型被锁死为 T,无法"分支"
}
真实需求:类型需要根据类型参数"做判断"——就像值需要根据条件做判断一样。
1.2 一个真实痛点:API 解包
大屏项目的接口层,你一定写过这种代码:
// 不同接口返回的 data 包装层数不一样:
// 有的直接返回设备数组:Device[]
// 有的返回 Promise:Promise<Device[]>
// 有的返回分页:Page<Device>
// 想写一个统一的"取数据"工具,返回类型怎么标注?
function unwrap<T>(input: T): ??? {
// 如果 T 是 Promise<X>,返回 X
// 如果 T 是数组,返回数组本身
// 否则原样返回
}
返回类型里有"如果"——这就是条件类型的用武之地。
二、条件类型基本语法
2.1 语法与读法
// 值的三元:条件 ? 真值 : 假值
// 类型的三元:T extends U ? X : Y
type IsNumber<T> = T extends number ? true : false;
type A = IsNumber<number>; // true —— 注意是字面量类型 true!
type B = IsNumber<string>; // false
type C = IsNumber<65>; // true —— 65 是 number 的子类型
type D = IsNumber<Device>; // false
读法:T extends U ? X : Y = “如果 T 能赋值给 U(T 是 U 的子类型),结果是 X,否则是 Y”。
这里的 extends 和 Day 9 的泛型约束是同一个关键字、同一个语义——判断"是否满足结构",只是用途从"立规矩"变成了"做判断"。
2.2 extends 的判断本质:结构兼容
interface Device { id: string; temp: number }
interface CNC extends Device { axis: number }
// CNC 有 Device 的全部结构 → CNC extends Device 成立
type X = CNC extends Device ? "是子类型" : "不是";
// X = "是子类型"
// 反过来不成立:Device 缺 axis
type Y = Device extends CNC ? "是子类型" : "不是";
// Y = "不是"
// 字面量是宽类型的子类型
type Z = "running" extends string ? true : false; // true
type W = 65 extends number ? true : false; // true
2.3 条件类型必须配合泛型使用
// ❌ 条件类型不能脱离类型参数独立存在
// type Bad = number extends string ? 1 : 2; // 无意义(TS 允许但结果恒定)
// ✅ 它存在的意义:作为泛型的"类型运算逻辑"
type TypeName<T> =
T extends string ? "string"
: T extends number ? "number"
: T extends boolean ? "boolean"
: T extends Function ? "function"
: "object";
type T1 = TypeName<"running">; // "string"
type T2 = TypeName<65>; // "number"
type T3 = TypeName<Device>; // "object"
type T4 = TypeName<() => void>; // "function"
三、嵌套条件类型:类型分支树
3.1 嵌套写法
/**
* 根据状态类型决定展示配置的类型
* 嵌套条件 = 值层面嵌套三元,逻辑一样,可读性同样需要控制层级
*/
type StatusStyle<S> =
S extends "fault"
? { color: "#ff4444"; blink: true; priority: 1 } // 故障:红色闪烁
: S extends "offline"
? { color: "#7a8ba0"; blink: false; priority: 3 } // 离线:灰色静止
: { color: "#00ff88"; blink: false; priority: 2 }; // 其他:绿色
type FaultStyle = StatusStyle<"fault">;
// { color: "#ff4444"; blink: true; priority: 1 }
3.2 工业场景:根据载荷类型选渲染器
interface Device { id: string; temp: number }
interface AlertRecord { id: string; reason: string; level: 1 | 2 | 3 }
/**
* 渲染器选择:不同数据类型对应不同组件的 Props 类型
* 这是"类型驱动的组件分发"——Vue/React 泛型组件的核心思路
*/
type RendererProps<T> =
T extends Device
? { type: "card"; device: T } // 设备 → 卡片组件
: T extends AlertRecord
? { type: "banner"; alert: T; dismissable: true } // 告警 → 横幅组件
: { type: "fallback"; data: T }; // 兜底
// 类型层面就锁定了"设备必须配卡片、告警必须配横幅"
const p1: RendererProps<Device> = { type: "card", device: { id: "CNC-001", temp: 65 } };
const p2: RendererProps<AlertRecord> = {
type: "banner",
alert: { id: "1", reason: "过热", level: 2 },
dismissable: true
};
// p1 想用 banner?类型不匹配,编译报错 ✅
四、分配律:最重要也最易错的机制
4.1 什么是分配律
核心规则:当条件类型的检查对象是裸类型参数(naked type parameter),且传入联合类型时,TS 会把联合拆开逐个成员计算,再把结果合并成新联合。
type ToArray<T> = T extends any ? T[] : never;
// 传入联合 → 分配律启动:
type R = ToArray<string | number>;
// 内部计算过程(脑内模拟):
// 第一步:拆分联合
// ToArray<string> | ToArray<number>
// 第二步:逐个计算
// string[] | number[]
// 最终结果:
type R_final = string[] | number[]; // ⭐ 不是 (string | number)[]!
// 验证:
const r1: R = ["a", "b"]; // ✅ 是 string[]
const r2: R = [1, 2]; // ✅ 是 number[]
const r3: R = ["a", 1]; // ❌ 混合数组不合法!
4.2 为什么要分配?想想 Exclude 的需求
// 需求:从状态联合里排除 "fault"
type Status = "running" | "standby" | "fault" | "offline";
// 期望结果:"running" | "standby" | "offline"
// —— 本质就是"逐个检查每个成员,决定留不留"
// 分配律正好干这个:
type MyExclude<T, U> = T extends U ? never : T;
type NotFault = MyExclude<Status, "fault">;
// 脑内模拟分配过程:
// "running" extends "fault" ? never : "running" → "running"(留下)
// "standby" extends "fault" ? never : "standby" → "standby"(留下)
// "fault" extends "fault" ? never : "fault" → never (干掉)
// "offline" extends "fault" ? never : "offline" → "offline"(留下)
// 合并:"running" | "standby" | "offline" ✅
没有分配律,就没有 Exclude/Extract/NonNullable——它们全是分配律的应用。
4.3 阻止分配:方括号包裹
// 有时你想"把整个联合当一个整体判断",不想要分配
type IsArray<T> = T extends any[] ? true : false;
type X = IsArray<string | string[]>;
// 分配启动:IsArray<string> | IsArray<string[]>
// = false | true
// = boolean ⚠️ 不是你想要的 true!
// ✅ 阻止分配:用元组包住 T(T 不再是"裸"的)
type IsArrayWhole<T> = [T] extends [any[]] ? true : false;
type Y = IsArrayWhole<string | string[]>;
// [string | string[]] extends [any[]] —— 整体判断
// string | string[] 能赋给 any[] 吗?string 不行 → false
type Y_final = false;
一句话记忆:
裸 T + 联合 = 自动拆开逐个算(过滤场景:Exclude)
[T] + 联合 = 整体判断不拆(判等场景:IsEqual)
4.4 判等的正确姿势:IsEqual
// ❌ 常见错误写法:被分配坑
type Eq1<T, U> = T extends U ? true : false;
type Bad = Eq1<string | number, string>; // false | true = boolean ⚠️
// ✅ 经典 IsEqual 实现(双向 extends)
type IsEqual<T, U> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2)
? true
: false;
// 原理:利用函数类型的内部同一性比较(TS 的官方技巧,先记住用法)
type Good1 = IsEqual<string, string>; // true
type Good2 = IsEqual<string, number>; // false
type Good3 = IsEqual<string | number, string>; // false ✅ 整体判断
五、never 的吸收:联合过滤的秘密
5.1 never 在联合中的特殊行为
// never 是"空类型"——联合中遇到它会被自动吸收
type A = "running" | never; // "running"
type B = never | never; // never
type C = string | number | never; // string | number
// 就像值运算:x + 0 = x,任何值 | never = 自身
// Exclude 正是利用这一点"删除"联合成员:
// 不想要的成员被映射成 never,合并时自动消失!
5.2 分配律 + never = 联合过滤器
/**
* 通用"按条件过滤联合"模式
*/
type FilterOut<T, U> = T extends U ? never : T;
// 场景:状态联合里排除所有"异常态"
type DeviceStatus = "running" | "standby" | "fault" | "offline";
type Abnormal = "fault" | "offline";
type Normal = FilterOut<DeviceStatus, Abnormal>;
// "running" | "standby" ✅
// 场景:告警级别里排除最低级(不推送的)
type AlertLevel = 1 | 2 | 3 | 4;
type PushLevel = FilterOut<AlertLevel, 4>;
// 1 | 2 | 3
5.3 空联合 = never:防不住的边界
// 把所有成员都过滤掉,结果是 never
type Nothing = FilterOut<"a" | "b", "a" | "b">; // never
// 实际影响:never 类型无法实例化
// const x: Nothing = ??? —— 没有任何值满足 never
// 如果业务上"不可能为空",过滤后要留意这个边界
六、infer:模式匹配中的捕获变量
6.1 从官方 ReturnType 源码说起
第 6 天你用过 ReturnType<typeof fn>,今天看它的源码(TS 内置库 lib.es5.d.ts):
// 官方源码:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
infer R 是什么?在模式匹配中声明一个"待推断的占位类型"。
6.2 infer 的语义:正则捕获组类比
值层面的正则:
/Promise<(.+)>/.exec("Promise<Device>") → 捕获组 1 = "Device"
类型层面的 infer:
T extends Promise<infer V> ? V : never
↑ ↑
待匹配的模式 V 是"内部类型位置"的捕获变量
T = Promise<Device> 时:
模式匹配成功 → V 被绑定为 Device → 返回 V(即 Device)
读法:“如果 T 长得像 Promise<某类型>,把那个’某类型’抓出来叫 V,结果是 V”。
6.3 最小示例:逐步体会
/** 解包 Promise:是就取内部,不是就原样返回 */
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type U1 = Unwrap<Promise<Device>>; // Device ⭐ 匹配成功,V = Device
type U2 = Unwrap<Device>; // Device ⭐ 匹配失败,走 else 分支
type U3 = Unwrap<Promise<Device[]>>; // Device[] ⭐ V 捕获整个内部类型
/** 数组元素提取 */
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
type E2 = ElementOf<Device[]>; // Device
type E3 = ElementOf<string>; // never(不是数组,没有元素可言)
6.4 infer 的三条使用规则
// 规则 1:infer 只能出现在 extends 右侧的【模式位置】
type Bad<T> = T extends any ? infer R : never;
// ❌ 报错:R 没有绑定任何模式位置,编译器不知道要捕获什么
type Good<T> = T extends Promise<infer V> ? V : never; // ✅
// 规则 2:infer 的变量只在当前条件类型的作用域内有效
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
// F 只能在 ? 后的分支里使用
// 规则 3:同一分支里可以捕获多个变量
type PairTypes<T> = T extends [infer A, infer B] ? [A, B] : never;
type P = PairTypes<[string, number]>; // [string, number]
七、infer 的六种捕获位置
7.1 数组元素
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<Device[]>; // Device
type C = ElementOf<[number, string]>; // number | string(元组也会分配!)
// ⚠️ 注意:元组 [number, string] 传入时,分配律会拆成两个成员
// [number] extends E[]? → E = number;[string] 同理 → 联合
7.2 Promise 内部
type Awaited2<T> = T extends Promise<infer V> ? V : T;
type A1 = Awaited2<Promise<Device[]>>; // Device[]
type A2 = Awaited2<string>; // string(不匹配原样返回)
type A3 = Awaited2<Promise<Promise<number>>>; // Promise<number>
// ⚠️ 只解一层!多层解包要用递归(见第八节)
7.3 函数参数
/** 函数第一个参数 */
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F1 = FirstArg<(id: string, temp: number) => void>; // string
type F2 = FirstArg<() => void>; // never(无参数)
/** 完整参数元组(官方 Parameters 的原理) */
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type P1 = MyParameters<(a: string, b: number) => void>;
// [a: string, b: number] —— 带标签的元组!
/** 元组的最后一项 */
type LastOf<T extends any[]> = T extends [...any[], infer L] ? L : never;
type L1 = LastOf<[string, number, boolean]>; // boolean
7.4 函数返回值
/** 官方 ReturnType 的原理 */
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
function fetchDevices(): Promise<Device[]> { /* ... */ return Promise.resolve([]); }
type R1 = MyReturnType<typeof fetchDevices>; // Promise<Device[]>
type R2 = Awaited<R1>; // Device[](先取返回值再解包)
type R3 = Awaited2<MyReturnType<typeof fetchDevices>>; // 一步到位:Device[]
7.5 对象的值类型
/** 捕获 Record 的值类型 */
type Values<T> = T extends Record<string, infer V> ? V : never;
type V1 = Values<{ a: string; b: number }>; // string | number ⚠️(宽松版)
// ⚠️ 这个写法有坑:具体对象类型 { a: string; b: number }
// 匹配 Record<string, infer V> 时 V 会被推断为值的联合
// 严格版要配合 Day 13 的映射类型:T[keyof T]
type StrictValues<T> = T[keyof T];
type V2 = StrictValues<{ a: string; b: number }>; // string | number ✅
7.6 元组 / 字符串的片段
/** 元组前半段与后半段 */
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type H1 = Head<[string, number, boolean]>; // string
type T1 = Tail<[string, number, boolean]>; // [number, boolean]
/** 字符串前缀提取(模板字面量 + infer,Day 13 详解) */
type ExtractDomain<E extends string> = E extends `${infer D}:${string}` ? D : never;
type D1 = ExtractDomain<"device:update">; // "device"
type D2 = ExtractDomain<"alert:remove">; // "alert"
type D3 = ExtractDomain<"无冒号">; // never
八、递归条件类型:类型的自引用
8.1 条件类型可以引用自己
TS 4.1 起,条件类型允许递归引用——就像函数可以调用自己。这是"类型体操"的分水岭能力:
/** 无限层解 Promise */
type DeepAwaited<T> = T extends Promise<infer V> ? DeepAwaited<V> : T;
// ↑ 自己调用自己!
type D1 = DeepAwaited<Promise<Promise<Promise<Device>>>>; // Device ⭐ 三层全解
type D2 = DeepAwaited<Device>; // Device(不匹配直接返回)
type D3 = DeepAwaited<Promise<Device[]>>; // Device[]
执行过程脑内模拟(DeepAwaited<Promise<Promise<Device>>>):
第 1 轮:Promise<Promise<Device>> 匹配 Promise<infer V> → V = Promise<Device>
→ 结果 = DeepAwaited<Promise<Device>>
第 2 轮:Promise<Device> 匹配 → V = Device
→ 结果 = DeepAwaited<Device>
第 3 轮:Device 不匹配 → 走 else → Device ✅ 递归终止
8.2 深度扁平化数组(进阶感受)
/** 无限层打平嵌套数组 */
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
type F1 = Flatten<string[][]>; // string
type F2 = Flatten<[number, [string]]>; // number | string
type F3 = Flatten<Device>; // Device(不是数组,终止)
8.3 今天的递归只需看懂
递归 + infer + 分配律的组合是第 3 周类型体操的主战场(DeepReadonly、TupleToUnion、字符串解析……)。今天的目标是看懂执行过程,写法细节下周再战。
九、工业实战场景全覆盖
9.1 场景一:接口返回值的自动解包
/**
* 大屏项目的统一请求层:
* 无论接口返回 Promise<Device[]> 还是 Promise<Page<Device>>
* 都能精确推导出 data 的类型
*/
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as Promise<T>;
}
/** 自动解包:请求函数的返回值类型 → 解掉 Promise */
type ApiData<F> = Awaited<ReturnType<F>>;
async function fetchDevices(): Promise<Device[]> { /* ... */ }
async function fetchStats(): Promise<DeviceStats> { /* ... */ }
type DevicesData = ApiData<typeof fetchDevices>; // Device[]
type StatsData = ApiData<typeof fetchStats>; // DeviceStats
// API 类型永不撒谎:改了 fetchDevices 的返回值,DevicesData 自动同步
9.2 场景二:WebSocket 消息的类型分发
/**
* 智能工厂消息协议:不同 type 对应不同载荷
* 条件类型根据 type 字段反查载荷类型
*/
interface MessageMap {
device_update: { deviceId: string; patch: Partial<Device> };
alert_raise: { level: 1 | 2 | 3; reason: string };
stats_refresh: { ts: number };
}
/** 消息的判别联合(D3 知识复习)*/
type WsMessage = {
[K in keyof MessageMap & string]: { type: K; payload: MessageMap[K] };
}[keyof MessageMap & string];
/** 根据 type 字符串查载荷类型(条件类型 + infer) */
type PayloadOf<T extends string> =
T extends keyof MessageMap ? MessageMap[T] : never;
type P1 = PayloadOf<"device_update">; // { deviceId: string; patch: Partial<Device> }
type P2 = PayloadOf<"alert_raise">; // { level: 1 | 2 | 3; reason: string }
/**
* 类型安全的消息发送
*/
function send<T extends keyof MessageMap & string>(
type: T,
payload: PayloadOf<T>
): void {
// 发送逻辑……
}
send("alert_raise", { level: 2, reason: "主轴过热" }); // ✅ 载荷精确校验
send("alert_raise", { level: 5 }); // ❌ level 越界 + 缺 reason,编译报错
9.3 场景三:组件 Props 的条件联动
/**
* 图表组件:mode 决定 data 的类型
* "line" 模式要时序点数组,"pie" 模式要名称-数值对
*/
interface ChartProps<M extends "line" | "pie"> {
mode: M;
data: M extends "line"
? { ts: number; value: number }[] // 折线:时序数据
: { name: string; value: number }[]; // 饼图:占比数据
title: string;
}
// 使用:mode 传字面量,data 类型自动锁定
const lineChart: ChartProps<"line"> = {
mode: "line",
title: "温度趋势",
data: [{ ts: 1, value: 65 }, { ts: 2, value: 68 }] // ✅
};
const pieChart: ChartProps<"pie"> = {
mode: "pie",
title: "状态分布",
data: [{ name: "运行", value: 120 }] // ✅
};
// lineChart.data = [{ name: "x", value: 1 }]; // ❌ 模式与数据不匹配!
9.4 场景四:条件类型做"字段类型过滤"(预告 Day 13)
/**
* 找出 Device 中类型为 number 的字段名
* 分配律 + 条件类型 + never 吸收的组合拳
*/
interface Device {
id: string;
temp: number;
battery: number;
name: string;
}
type NumericKeys<T> = {
[K in keyof T]: T[K] extends number ? K : never; // number 字段留名,否则 never
}[keyof T]; // 索引访问取所有值的联合
type DeviceNumeric = NumericKeys<Device>;
// "temp" | "battery" ⭐(id/name 是 string,被 never 吸收掉了)
// 应用:只能对数字字段求和的函数
function sumField<T, K extends NumericKeys<T>>(arr: T[], key: K): number {
return arr.reduce((s, item) => s + (item[key] as number), 0);
}
sumField(devices, "temp"); // ✅
sumField(devices, "name"); // ❌ name 不是 number 字段,编译报错
这段代码同时用到了映射类型 [K in keyof T](Day 13 主角)——条件类型与映射类型的组合,就是所有高级工具类型的 DNA。
十、类比记忆:海关查验 vs 正则捕获
条件类型 = 海关查验通道
┌─────────────────────────────────────┐
│ 旅客 T 走到关口 │
│ "你符合 U 国签证吗?"(T extends U) │
│ 符合 → 走 X 通道(? X) │
│ 不符合 → 走 Y 通道(: Y) │
│ │
│ 旅行团(联合类型)= 逐个查验 │
│ 每人单独过闸(分配律) │
│ 拒签者标记 never,名单自动划掉 │
└─────────────────────────────────────┘
infer = 安检的"取样袋"
┌─────────────────────────────────────┐
│ 行李 T = Promise<Device> │
│ 模式:Promise<取样袋> │
│ 打开行李 → 取样袋装进 Device │
│ 之后用取样袋里的东西(V = Device) │
└─────────────────────────────────────┘
十一、常见坑点与最佳实践
坑点 1:裸参数分配引发的意外
// 想判断"是不是数组",结果被分配坑了
type IsArray<T> = T extends any[] ? true : false;
type X = IsArray<string | string[]>; // boolean ⚠️ 不是 true!
// ✅ 阻止分配
type IsArray2<T> = [T] extends [any[]] ? true : false;
type Y = IsArray2<string | string[]>; // true(整体判断)
判断口诀:写完条件类型,永远用一个联合类型测试一下——看结果是不是被拆开了。
坑点 2:infer 声明了却不在模式位置
// ❌ infer 没绑定任何待匹配的位置
type Bad<T> = T extends any ? infer R : never;
// 编译错误:'R' is declared but never used / 无法推断
// ✅ infer 必须出现在 extends 右侧的模式中
type Good<T> = T extends Promise<infer V> ? V : never;
坑点 3:boolean 是 true | false 的联合
type IsTrue<T> = T extends true ? 1 : 2;
type R = IsTrue<boolean>;
// boolean = true | false → 分配!
// IsTrue<true> | IsTrue<false> = 1 | 2 ⚠️ 意外的联合
// ✅ 想整体判断
type IsTrue2<T> = [T] extends [true] ? 1 : 2;
type R2 = IsTrue2<boolean>; // 2 ✅
扩展:boolean 在 TS 内部就是 true | false,任何对 boolean 的裸分配都会拆成两个成员。
坑点 4:never 传入条件类型直接消失
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<never>; // never ⚠️ 不是 never[]!
// 原因:never 是"空联合"——分配律拆开 0 个成员,合并 0 个 = never
// 这个特性会被用于判断"是否为 never":
type IsNever<T> = [T] extends [never] ? true : false;
type X = IsNever<never>; // true ✅(方括号阻止分配)
type Y = IsNever<string>; // false
坑点 5:递归没有终止条件
// ❌ 无限递归(TS 会报 Type instantiation is excessively deep)
type Loop<T> = T extends object ? Loop<T> : T;
// T 是 object 时永远匹配 object → 无限自调用
// ✅ 每轮递归必须"缩小问题规模"
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> } // 每轮深入一层字段
: T;
坑点 6:条件类型太复杂导致编译报错
// TS 对类型实例化深度有上限(约 50 层递归 / 500 万次实例化)
// 深度递归类型(如超长字符串解析)可能触发:
// "Type instantiation is excessively deep and possibly infinite"
// 排查思路:
// 1. 递归是否每轮缩小规模?
// 2. 是否不小心写成了"左右互搏"(互相 extends 的循环)?
// 3. 把复杂类型拆成中间类型,逐层验证
最佳实践清单
分配是特性不是 bug——写过滤用裸 T,写判等用
[T] extends [U]infer 的命名要表意——
infer Return、infer Element比infer R、infer E可读复杂条件类型拆中间类型——
type Step1 = ...; type Step2 = ...逐步验证先在 Playground 用悬停验证每一步的推导结果,再放进项目
条件类型嵌套不超过 3 层——超过就拆成具名中间类型
业务代码优先用官方工具类型,自定义条件类型留给"官方没有的变换"
十二、自测挑战
Q1:T extends U ? X : Y 中 extends 的语义是什么?和泛型约束里的 extends 是什么关系?
Q2:写出以下代码的结果并解释原因:
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;
Q3:ToArray<string | number> 和 [T] extends [any] ? T[] : never 版本的结果分别是什么?为什么不同?
Q4:为什么 Exclude<T, U> 能"删除"联合成员?说出 never 在其中的两个作用。
Q5:手写 MyReturnType<T>(不许翻第六节),并用 typeof fetchDevices 验证。
Q6:写一个 SecondArg<T>——取函数第二个参数的类型。(a: string, b: number, c: boolean) => void 应得 number。
Q7:type IsNever<T> = [T] extends [never] ? true : false 为什么必须加方括号?不加会怎样?
Q8:DeepAwaited<Promise<Promise<Device>>> 的递归执行过程分几轮?每轮的结果是什么?
Q9:IsTrue<boolean>(裸参数版)的结果为什么是 1 | 2 而不是 2?
Q10:设计一个条件类型 ChartProps<M>,让 mode: "line" 时 data 必须是 { ts: number; value: number }[],mode: "bar" 时必须是 { label: string; value: number }[]。
十三、总结与知识图谱
条件类型与 infer(类型层面的 if/else 与捕获)
│
├── 条件类型
│ ├── 语法:T extends U ? X : Y(子类型判断 → 分支)
│ ├── 嵌套:类型分支树(RendererProps 分发)
│ └── 必须配合泛型(类型参数是判断的输入)
│
├── 分配律(最核心机制)
│ ├── 裸 T + 联合 → 拆开逐个算,结果合并
│ ├── [T] extends [U] → 整体判断(阻止分配)
│ ├── boolean = true | false(天然的联合)
│ └── never 传入 → 直接得 never(空联合拆 0 个)
│
├── never 的吸收
│ ├── A | never = A(联合中自动消失)
│ ├── 分配 + never = 联合过滤器(Exclude 原理)
│ └── 全部滤光 → never(无法实例化的边界)
│
├── infer(捕获变量)
│ ├── 语义:模式中占位,匹配成功绑定实际类型
│ ├── 类比:正则捕获组
│ ├── 六种位置:数组元素 / Promise 内部 / 函数参数
│ │ / 返回值 / 对象值 / 元组与字符串片段
│ └── 规则:只能在 extends 右侧模式位置、分支内有效、可多个
│
├── 递归条件类型
│ ├── 自引用:DeepAwaited / Flatten
│ ├── 每轮必须缩小规模(防无限递归)
│ └── 类型体操的门票(第 3 周主战场)
│
└── 工业实战
├── ApiData<F>:接口返回值自动解包
├── PayloadOf<T>:消息协议类型分发
├── ChartProps<M>:mode 联动 data 类型
└── NumericKeys<T>:字段类型过滤(映射 + 条件组合)
一句话总结:条件类型是类型层面的三元运算符,分配律让它天然擅长过滤联合,infer 让它能"拆开"复杂类型取内部零件——三者合一,官方工具类型的源码从此对你透明。
延伸阅读
下一步
明天(Day 13)学习映射类型与模板字面量类型——[K in keyof T] 的遍历魔法、as 键重映射、-? 修饰符操作,以及字符串层面的类型运算。学完明天,你就能手写全部官方工具类型(Partial/Pick/Omit 全家桶),为 Day 14 的 BOSS 战备足弹药。
类型体操的第一课:先学会"看懂"执行过程,再谈"写对"。
每天花 2 小时,28 天通关 TypeScript 深入。加油!