【TS】day12-conditional-types-and-infer

作者:mario 发布时间: 2026-08-28 阅读量:6 评论数:0

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. 把复杂类型拆成中间类型,逐层验证

最佳实践清单

  1. 分配是特性不是 bug——写过滤用裸 T,写判等用 [T] extends [U]

  2. infer 的命名要表意——infer Returninfer Elementinfer Rinfer E 可读

  3. 复杂条件类型拆中间类型——type Step1 = ...; type Step2 = ... 逐步验证

  4. 先在 Playground 用悬停验证每一步的推导结果,再放进项目

  5. 条件类型嵌套不超过 3 层——超过就拆成具名中间类型

  6. 业务代码优先用官方工具类型,自定义条件类型留给"官方没有的变换"


十二、自测挑战

Q1T extends U ? X : Y 中 extends 的语义是什么?和泛型约束里的 extends 是什么关系?

Q2:写出以下代码的结果并解释原因:

type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;

Q3ToArray<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

Q7type IsNever<T> = [T] extends [never] ? true : false 为什么必须加方括号?不加会怎样?

Q8DeepAwaited<Promise<Promise<Device>>> 的递归执行过程分几轮?每轮的结果是什么?

Q9IsTrue<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 让它能"拆开"复杂类型取内部零件——三者合一,官方工具类型的源码从此对你透明。


延伸阅读

资源

说明

TS Handbook - Conditional Types

官方文档(含分配律)

TS Handbook - inferring in Conditional Types

infer 官方详解

TypeScript Playground

悬停验证所有推导

type-challenges: Awaited / Parameters / ReturnType

今天的知识明天就能做(easy 难度)


下一步

明天(Day 13)学习映射类型与模板字面量类型——[K in keyof T] 的遍历魔法、as 键重映射、-? 修饰符操作,以及字符串层面的类型运算。学完明天,你就能手写全部官方工具类型(Partial/Pick/Omit 全家桶),为 Day 14 的 BOSS 战备足弹药。


类型体操的第一课:先学会"看懂"执行过程,再谈"写对"。

每天花 2 小时,28 天通关 TypeScript 深入。加油!


评论