跳到主要内容

第 16 章 · 高级条件类型与解析

读完本章你会
  • [T] 包装控制分布式,写出 IsNeverIsUnion 这类「判定类型」
  • 理解 any 的特殊性,写出 IsAny
  • 实现 UnionToIntersection,看懂「逆变位置」这个反直觉技巧
  • 用模板 + 递归编写字符串解析器(URL 参数、Query String)
本章是全书最「硬核」的一章

前面的招式都是「顺向」用类型;本章开始反着用——把分布式、逆变、any 的怪异行为当成工具。看懂这章,你就跨进了类型系统的高级玩家区。


16.1 判定类型:IsNever / IsUnion

16.1.1 IsNever:为什么不能直接 T extends never

直觉是 type IsNever<T> = T extends never ? true : false,但它是错的:

type BadIsNever<T> = T extends never ? true : false
type A = BadIsNever<never> // never,不是 true!

// 原因:T 是裸类型参数,never 分发后是「空」,整个条件求值为 never

正确写法是把 T 包进元组,关闭分发:

type IsNever<T> = [T] extends [never] ? true : false
type A = IsNever<never> // true
type B = IsNever<string> // false
教训

任何「判定 never」的题,第一步都是 [T] extends [never]。这是分发机制的直接后果,也是判断类型最常用的防御。

16.1.2 IsUnion:分发时「别人还在吗」

判断一个类型是不是联合,靠的是「分发时能不能看到其他成员」:

type IsUnion<T, U = T> =
[T] extends [never] ? false // never 不是联合
: T extends any // 分发:对每个成员单独看
? [U] extends [T] ? false : true // 若 [U](完整联合)不 extends [T](单成员)
: never // 说明 T 不止一个成员 → 是联合
type A = IsUnion<string> // false(单成员)
type B = IsUnion<string | number> // true(分发时 [U] 不是单成员)
type C = IsUnion<string | 'a'> // false('a' 被 string 吸收,退化成 string)
type D = IsUnion<never> // false
关键一行:[U] extends [T]

U 是完整的联合(存在默认参数里),T 分发后是单成员。若 [U] extends [T] 成立,说明「完整联合就是这一个成员」→ 不是联合;不成立 → 联合里有别的成员 → 是联合。用「完整 vs 单成员」的包含关系反推「是否联合」,是本章第一个反直觉技巧。


16.2 IsAny:识别类型黑洞

any 有两个「特殊性质」:它 extends 任何类型、也被任何类型 extends。利用其中一个:

// any & 1 = any,而 0 extends any 恒成立
type IsAny<T> = 0 extends (1 & T) ? true : false

type A = IsAny<any> // true
type B = IsAny<unknown> // false
type C = IsAny<never> // false
type D = IsAny<string> // false
为什么这样能区分 any

1 & unknown = 1unknown 被吸收),0 extends 1 为假 → IsAny<unknown> 是 false。 1 & any = any0 extends any 恒真 → IsAny<any> 是 true。 只有 any 能同时「吞掉 1 又让 0 extends 成立」——这就是 any 的指纹。


16.3 UnionToIntersection:把联合变交叉

UnionToIntersection<'a' | 'b'>'a' & 'b'。方法很反直觉——利用函数参数的逆变位置

type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never

type R = UnionToIntersection<'a' | 'b'> // 'a' & 'b'
type F = UnionToIntersection<(() => 'a') | (() => 'b')>
// (() => 'a') & (() => 'b')
原理:逆变位置的联合 = 交叉
  1. 先把联合的每个成员变成「参数位置」:(k: 'a') => void | (k: 'b') => void
  2. infer I 反推这个「函数联合」的参数
  3. 函数参数是逆变位置:能同时接受 'a''b' 的参数,类型必须是 'a' & 'b'——联合在逆变位被「压」成了交叉

这是 TS 类型系统最著名的「巧劲」之一。逆变位联合取交、协变位联合取并,记住这句。


16.4 字符串解析器:模板 + 递归

16.4.1 ParseUrlParams:抽出 :param

type ParseUrlParams<T> =
T extends `${infer L}:${infer R}`
? R extends `${infer P}/${infer Rest}`
? P | ParseUrlParams<Rest>
: R
: never

type A = ParseUrlParams<'posts/:id/:user'>
// 'id' | 'user'
type B = ParseUrlParams<'/users/:id/edit'> // 'id'
type C = ParseUrlParams<''> // never
解析器通用形态

「先匹配可辨识前缀(:),再拆出内容(P)和后缀(Rest),递归处理后缀」。P | ParseUrlParams<Rest> 把每段的参数并成联合。09616 就是它。

16.4.2 QueryStringParser:最 extreme 的一道

ParseQueryString<'k1=v1&k2=v2&k1=v2'>{ k1: ['v1', 'v2'], k2: 'v2' }。它是「按 & 拆段 + 每段解析成键值 + 逐段合并(同键并入数组去重)」的组装:

// 1. 一段 'k=v' 或 'k' → { k: v } 或 { k: true }
type ParseOne<S extends string> =
S extends `${infer K}=${infer V}` ? { [P in K]: V } : { [P in S]: true }

// 2. 合并两个对象:同键的值并成数组(去重)
type Merge<A, B> = {
[K in keyof A | keyof B]:
K extends keyof A
? K extends keyof B
? A[K] extends B[K] ? A[K] : [A[K], B[K]]
: A[K]
: K extends keyof B ? B[K] : never
}

// 3. 整体:按 & 拆段,逐段 Merge
type ParseQueryString<S extends string> =
S extends ''
? {}
: S extends `${infer A}&${infer B}`
? Merge<ParseOne<A>, ParseQueryString<B>>
: ParseOne<S>
这道题为什么是 extreme

它把本章 / 前几章的手法制成一锅:模板拆段 + 键值解析 + 映射合并 + 数组去重。00151 的「值相等保持单值、不相等并成数组」是它最刁钻的地方。不求一次写对,先按这三步搭骨架


本章小结

  • [T] extends [never] 关闭分发,是判定类型的起点
  • IsUnion 用「完整 vs 单成员」反推是否联合
  • IsAny0 extends (1 & T) 识别 any 指纹
  • UnionToIntersection 用逆变位把联合压成交叉
  • 解析器 = 模板匹配 + 递归 + 分步组装;ParseUrlParams / QueryStringParser 是两道范例题

本章练习

挑战题

题号题目难度考察点
01042IsNevermedium[T] extends [never]
01097IsUnionmedium完整 vs 单成员
00223IsAnyhard0 extends (1 & T)
00055联合类型转化为交叉类型hard逆变位
09616Parse URL Paramsmedium模板解析
00151Query String Parserextreme拆段 + 合并 + 去重

代码练习

  1. IsNever 陷阱:先写出错误版本 T extends never ? true : false,验证 IsNever<never>never,再改成 [T] extends [never] 并验证。

  2. IsUnion 变形:实现 IsUnion 后,验证 IsUnion<boolean>true(boolean 内部是 true|false)、IsUnion<never>false

  3. UnionToIntersection 变形:实现 UnionToIntersection,验证对 'a' | 'b''a' & 'b',对 string | numberstring & number(= never,观察这个结果)。

  4. ParseUrlParams 扩展:实现 ParseUrlParams,验证 '' → never、':id' → 'id'、'posts/:id/:user/like' → 'id' | 'user'。

  5. QueryStringParser 简化版:先实现「按 & 拆段、无重复键」的简化版('k1=v1&k2=v2'{ k1: 'v1', k2: 'v2' }),跑通后再加「同键并数组」的 Merge,逐步逼近 00151 全解。