Yuzhe's Blog

yuzhes

IsUnion

IsUnion

题目链接

题目

实现类型 IsUnion,判断传入的类型 T 是否为联合类型。

type case1 = IsUnion<string>            // false
type case2 = IsUnion<string | number>   // true
type case3 = IsUnion<[string | number]> // false

解法

type IsUnion<T, U = T> = [T] extends [never]
  ? false
  : T extends T
    ? [U] extends [T]
      ? false
      : true
    : never

核心思路:“拷贝参数” + 分布式条件类型 + 非分布式比较三者联动。

逐步拆解

第一步 — 保留原始联合:U = T

在任何条件触发之前,把原始类型 T 备份到第二个参数 U。这一步至关重要——因为后续步骤会让 T 在分布时变成单个成员。

第二步 — 处理 never[T] extends [never]

never 在语义上是空联合,但我们希望 IsUnion<never> 返回 false。用 [] 包裹禁止分布,让条件可以正常命中 false 分支。

第三步 — 触发分布:T extends T

T extends T 看起来是废话,但它会触发分布式条件类型,把联合的每个成员单独处理。当 T = string | number 时,TypeScript 展开为两条分支:

分支T(当前成员)U(原始类型)
1stringstring | number
2numberstring | number

第四步 — 比较成员与原始:[U] extends [T]

[] 包裹阻止再次分布。此时问题变为:原始类型能否 extends 当前单个成员

所有分支结果合并为联合,只要有一条分支出现了 true,最终就是 true

深入理解

为什么”拷贝参数”能起作用?

关键在于:分布式条件类型只影响 被检查的类型参数T extends Constraint 里的 T),其他变量(U)在所有分支中始终保持原始值。

// 当 T = string | number,进入 T = string 的分支时:
//   U 仍然是 string | number ← 未被改变!
//   [U] extends [T] → [string | number] extends [string] → false ✓

没有 U,我们就无法引用分布前的原始联合,无法做这个比较。

[U] extends [T]U extends T 的区别

裸写 U extends T 会再次触发对 U 的分布展开,结果乱掉。必须用元组包裹做非分布式的单次比较。

// ❌ 错误 — 会对 U 再次分布
type Check = U extends T ? false : true

// ✅ 正确 — 元组阻止分布
type Check = [U] extends [T] ? false : true

外层 false : never 的作用

外层 T extends T 永远为真,never 分支永远不会进入。它只是为了满足 TypeScript 条件类型的语法要求。

边界情况

IsUnion<never>           // false — 被 [T] extends [never] 守卫
IsUnion<string>          // false — 单类型,U === T
IsUnion<boolean>         // true!boolean 内部是 true | false 的联合
IsUnion<string | never>  // false — never 折叠:string | never = string
IsUnion<[string|number]> // false — 联合在元组内,T 本身不是联合

boolean 这个边界案例很有趣:TypeScript 内部把 boolean 表示为 true | false 的联合,所以 IsUnion<boolean> 正确返回 true

核心要点