第 16 章 · 高级条件类型与解析
- 用
[T]包装控制分布式,写出IsNever、IsUnion这类「判定类型」 - 理解
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
1 & unknown = 1(unknown 被吸收),0 extends 1 为假 → IsAny<unknown> 是 false。
1 & any = any,0 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')
- 先把联合的每个成员变成「参数位置」:
(k: 'a') => void | (k: 'b') => void - 用
infer I反推这个「函数联合」的参数 - 函数参数是逆变位置:能同时接受
'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>
它把本章 / 前几章的手法制成一锅:模板拆段 + 键值解析 + 映射合并 + 数组去重。00151 的「值相等保持单值、不相等并成数组」是它最刁钻的地方。不求一次写对,先按这三步搭骨架。
本章小结
[T] extends [never]关闭分发,是判定类型的起点IsUnion用「完整 vs 单成员」反推是否联合IsAny用0 extends (1 & T)识别 any 指纹UnionToIntersection用逆变位把联合压成交叉- 解析器 = 模板匹配 + 递归 + 分步组装;
ParseUrlParams/QueryStringParser是两道范例题
本章练习
挑战题
| 题号 | 题目 | 难度 | 考察点 |
|---|---|---|---|
| 01042 | IsNever | medium | [T] extends [never] |
| 01097 | IsUnion | medium | 完整 vs 单成员 |
| 00223 | IsAny | hard | 0 extends (1 & T) |
| 00055 | 联合类型转化为交叉类型 | hard | 逆变位 |
| 09616 | Parse URL Params | medium | 模板解析 |
| 00151 | Query String Parser | extreme | 拆段 + 合并 + 去重 |
代码练习
-
IsNever 陷阱:先写出错误版本
T extends never ? true : false,验证IsNever<never>是never,再改成[T] extends [never]并验证。 -
IsUnion 变形:实现
IsUnion后,验证IsUnion<boolean>是true(boolean 内部是 true|false)、IsUnion<never>是false。 -
UnionToIntersection 变形:实现
UnionToIntersection,验证对'a' | 'b'得'a' & 'b',对string | number得string & number(= never,观察这个结果)。 -
ParseUrlParams 扩展:实现
ParseUrlParams,验证''→ never、':id'→ 'id'、'posts/:id/:user/like'→ 'id' | 'user'。 -
QueryStringParser 简化版:先实现「按 & 拆段、无重复键」的简化版(
'k1=v1&k2=v2'→{ k1: 'v1', k2: 'v2' }),跑通后再加「同键并数组」的Merge,逐步逼近 00151 全解。