Yuzhe's Blog

yuzhes

联合类型转交叉类型 Union to Intersection

联合类型转交叉类型 Union to Intersection

题目链接

题目

实现一个高级工具类型 UnionToIntersection<U>,将联合类型转换为交叉类型。

type I = UnionToIntersection<'foo' | 42 | true>
// 期望结果:'foo' & 42 & true

解答

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

这个方案利用了 TypeScript 的两个关键特性:分布式条件类型和函数参数位置的逆变推断

逐步拆解

第一步 — 将联合类型分布到函数类型中

U extends any ? (arg: U) => void : never

U 是联合类型 A | B | C 时,分布式条件类型会对每个成员独立映射:

((arg: A) => void) | ((arg: B) => void) | ((arg: C) => void)

U extends any 永远为真——它的作用纯粹是为了触发分布。

第二步 — 从逆变位置推断

... extends (arg: infer I) => void ? I : never

现在我们有一个函数的联合类型,需要从中推断出参数类型 I。TypeScript 必须找到一个类型 I,使得 (arg: I) => void 能被联合中的每个成员赋值。

函数参数是逆变的:如果 (arg: A) => void(arg: I) => void 的子类型,那么 I 必须是 A 的子类型(方向反转)。要同时满足所有三个函数,I 必须同时是 ABC 的子类型——这正好就是 A & B & C

推导过程:

U = 'foo' | 42 | true

第一步:((arg: 'foo') => void) | ((arg: 42) => void) | ((arg: true) => void)

第二步:推断 I 使其满足所有三个函数
        → I = 'foo' & 42 & true  ✓

深入分析

逆变是关键

TypeScript 的类型推断行为取决于 infer 所在位置的型变方向:

位置型变方向多个候选合并方式
返回类型协变联合(|
参数逆变交叉(&

如果把 infer 放在返回位置:

type Wrong<U> =
  (U extends any ? () => U : never) extends () => infer I ? I : never

type Test = Wrong<'foo' | 42>  // 'foo' | 42 — 还是联合类型!

返回位置是协变的,多个候选会合并为联合——跟原来一样。我们需要逆变的参数位置来将其翻转为交叉。

为什么需要 U extends any

没有分布式条件类型,U 会被当作一个整体处理:

type Broken<U> = ((arg: U) => void) extends (arg: infer I) => void ? I : never
type Test = Broken<'foo' | 42>  // 'foo' | 42 — 没有发生分布

extends any 包装是必须的,它让联合类型先分布成独立的函数类型。

实际应用场景

联合转交叉在合并配置对象时非常有用:

type Config = { host: string } | { port: number } | { debug: boolean }
type Merged = UnionToIntersection<Config>
// { host: string } & { port: number } & { debug: boolean }

它也是其他高级类型(如 UnionToTuple)的基础构建块。

不可能的交叉

当联合成员是不兼容的原始类型时,交叉会坍缩为 never

type I = UnionToIntersection<string | number>  // string & number → never

这是预期行为——没有值能同时是 stringnumber

核心要点