全排列
全排列
题目
实现联合类型的全排列,将联合类型转换成所有可能的全排列数组的联合类型。
type perm = Permutation<'A' | 'B' | 'C'>
// ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']解答
type Permutation<T, K = T> = [T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never这道题的核心在于两个 TypeScript 特性的组合:never 的终止条件和分发式条件类型。
逐步拆解
第一步 — 终止条件:[T] extends [never]
当所有元素都被消耗完后,T 会变成 never。但为什么要把它包在数组里?
// ❌ 错误 — 这会分发,永远不会触发
type Bad<T> = T extends never ? [] : ...
// ✅ 正确 — 包裹后禁止分发,能真正检测 never
type Good<T> = [T] extends [never] ? [] : ...因为 never extends never 会分发成「空操作」(没有任何分支能执行),所以必须用 [T] 包裹来关闭分发行为。
第二步 — 遍历联合类型:K extends K
K = T 是原始联合类型的一个副本。在 K extends K ? ... : never 中,TypeScript 会对 K 的每个成员分别处理。对于 K = 'A' | 'B' | 'C',会产生三个分支:
'A' extends 'A' | 'B' | 'C'→['A', ...Permutation<'B' | 'C'>]'B' extends 'A' | 'B' | 'C'→['B', ...Permutation<'A' | 'C'>]'C' extends 'A' | 'B' | 'C'→['C', ...Permutation<'A' | 'B'>]
第三步 — 用 Exclude<T, K> 递归
每个分支选取一个元素 K,然后用 Exclude<T, K> 去掉它,再对剩余元素递归。
Permutation<'A' | 'B' | 'C'> 的完整展开结果:
['A', 'B', 'C'] | ['A', 'C', 'B']
['B', 'A', 'C'] | ['B', 'C', 'A']
['C', 'A', 'B'] | ['C', 'B', 'A']深入理解
为什么 K extends K 能触发分发?
TypeScript 的分发式条件类型在以下情况激活:
- 被检查的类型是裸类型参数(没有被包裹)
- 该类型参数代表联合类型
K extends K 看起来是废话(永远为真),但它的目的就是触发分发。联合类型 K 的每个成员都会被单独处理,每个成员产生一个分支——这正是构造全排列所需要的结构。
never 的坑
never 是联合类型的单位元(never | A = A),也代表空集——遍历 never 不会产生任何结果。这导致一个微妙的问题:
// T = never
type Test = never extends never ? 'yes' : 'no'
// 结果:never(不是 'yes'!)
// 因为分发式 CT 对 0 个元素映射 → 产生 never用 [T] extends [never] 可以跳出分发,得到一个正常的条件判断。
复杂度
对于有 n 个成员的联合类型,Permutation 会生成 n! 个元组。TypeScript 有递归深度限制,因此对于小型联合类型(≤ 6-7 个成员)工作正常,较大时可能触发限制。
核心要点
[T] extends [never]是检测never而不触发分发的标准写法K extends K配合副本参数是触发分发、遍历联合类型成员的惯用写法Exclude<T, K>从剩余池中移除已选元素,用于下一层递归- TypeScript 的分发式条件类型能把单个类型表达式变成联合类型——这就是能用纯类型系统表达全排列的根本原因
