Yuzhe's Blog

yuzhes

Permutation

Permutation

Challenge Link

Challenge

Implement permutation type that transforms union types into the array that includes permutations of unions.

type perm = Permutation<'A' | 'B' | 'C'>
// ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

Solution

type Permutation<T, K = T> = [T] extends [never]
  ? []
  : K extends K
    ? [K, ...Permutation<Exclude<T, K>>]
    : never

The trick here is combining two TypeScript features: the never base case and distributive conditional types.

Breaking it down

Step 1 — Base case: [T] extends [never]

When all elements have been consumed, T becomes never. But why wrap it in an array?

// ❌ Wrong — this distributes and never fires
type Bad<T> = T extends never ? [] : ...

// ✅ Correct — wrapping disables distribution, actually checks never
type Good<T> = [T] extends [never] ? [] : ...

Because never extends never distributes into nothing (the branch is unreachable), we must wrap [T] to suppress distribution.

Step 2 — Distribute over the union: K extends K

K = T is a copy of the original union. When used in K extends K ? ... : never, TypeScript distributes over each member of K. For K = 'A' | 'B' | 'C', this produces three branches:

Step 3 — Recurse with Exclude<T, K>

Each branch picks one element K and recurses on the rest using Exclude<T, K>.

Full expansion for Permutation<'A' | 'B' | 'C'>:

['A', 'B', 'C'] | ['A', 'C', 'B']
['B', 'A', 'C'] | ['B', 'C', 'A']
['C', 'A', 'B'] | ['C', 'B', 'A']

Deep Dive

Why does K extends K distribute?

TypeScript’s distributive conditional types activate when:

  1. The checked type is a naked type parameter (not wrapped)
  2. The type parameter represents a union

K extends K looks redundant (it’s always true), but its purpose is purely to trigger distribution. Each member of the union K is processed separately, giving us one branch per element — exactly the structure we need to build permutations.

The never problem

never is the identity element of union types (never | A = A). It’s also the empty set — iterating over never yields nothing. This causes a subtle issue:

// T = never
type Test = never extends never ? 'yes' : 'no' 
// Result: never (not 'yes'!)
// Because distributive CT maps over 0 elements → produces never

By checking [T] extends [never], we escape distributivity and get a proper boolean-like branch.

Complexity

For a union of n members, Permutation generates n! tuples. TypeScript has recursion depth limits, so this works fine for small unions (≤ 6–7 members) but may hit limits for larger ones.

Key Takeaways

中文解析

类型定义解读

type Permutation<T, K = T> =
  [T] extends [never]           // 将 T 包裹进数组,禁止分布展开,检测 T 是否为 never(递归终止条件)
    ? []                        // T 已耗尽 → 返回空元组
    : K extends K               // K 是 T 的副本,这里触发分布式条件类型,对联合的每个成员单独处理
      ? [K, ...Permutation<Exclude<T, K>>]  // 选取当前成员 K,递归处理剩余 Exclude<T,K>
      : never

逐步分析

为什么需要两个类型参数 TK

如果只用 T 一个参数,T extends T 在递归时会把已缩小的 T 再次分布,但我们在递归调用中传递的是缩小后的值,所以实际上 K = T 的默认值让一切自洽。

关键技巧 1:[T] extends [never]——检测 never 的标准写法

// ❌ 错误写法
type Check<T> = T extends never ? 'yes' : 'no'
// 当 T = never 时,分布式展开产生 0 个分支,结果是 never(不是 'yes')

// ✅ 正确写法  
type Check<T> = [T] extends [never] ? 'yes' : 'no'
// 包裹后禁止分布,[never] extends [never] 正常求值为 true

关键技巧 2:K extends K——利用”永真条件”触发分布

K extends K 对任意类型 K 都为真,但它的作用不是过滤,而是触发分布式条件类型
K = 'A' | 'B' | 'C' 时,TypeScript 将其展开为:

| ('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'>])

最终得到 3! = 6 个元组的联合类型。

完整递归展开(以 'A'|'B'|'C' 为例)

Permutation<'A'|'B'|'C'>
├─ K='A': ['A', ...Permutation<'B'|'C'>]
│          ├─ K='B': ['A','B', ...Permutation<'C'>] → ['A','B','C']
│          └─ K='C': ['A','C', ...Permutation<'B'>] → ['A','C','B']
├─ K='B': ['B', ...Permutation<'A'|'C'>] → ['B','A','C'] | ['B','C','A']
└─ K='C': ['C', ...Permutation<'A'|'B'>] → ['C','A','B'] | ['C','B','A']

考察知识点