Yuzhe's Blog

yuzhes

Union to Intersection

Union to Intersection

Challenge Link

Challenge

Implement an advanced utility type UnionToIntersection<U> that converts a union type into an intersection type.

type I = UnionToIntersection<'foo' | 42 | true>
// expected: 'foo' & 42 & true

Solution

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

This solution exploits two key TypeScript behaviors: distributive conditional types and contravariant inference in function parameter positions.

Breaking it down

Step 1 — Distribute the union into function types

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

When U is a union like A | B | C, the distributive conditional type maps each member independently:

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

The U extends any is always true — it’s used purely to trigger distribution.

Step 2 — Infer from contravariant position

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

Now we have a union of functions and we’re trying to infer the parameter type I from all of them simultaneously. TypeScript must find a single type I such that (arg: I) => void is assignable from every member of the union.

Function parameters are contravariant: if (arg: A) => void is a subtype of (arg: I) => void, then I must be a subtype of A (the direction flips). For I to satisfy all three functions, it must be a subtype of A, B, and C simultaneously — which is exactly A & B & C.

Walkthrough:

U = 'foo' | 42 | true

Step 1: ((arg: 'foo') => void) | ((arg: 42) => void) | ((arg: true) => void)

Step 2: infer I where I satisfies all three
        → I = 'foo' & 42 & true  ✓

Deep Dive

Why contravariance is the key

TypeScript’s type inference behaves differently depending on the variance of the position where infer appears:

PositionVarianceMultiple candidates merge via
Return typeCovariantUnion (|)
ParameterContravariantIntersection (&)

If we put infer in the return position instead:

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

type Test = Wrong<'foo' | 42>  // 'foo' | 42 — still a union!

The return position is covariant, so multiple candidates merge into a union — exactly what we already had. We need the contravariant parameter position to flip it to an intersection.

Why U extends any?

Without the distributive conditional, U would be treated as a whole unit:

type Broken<U> = ((arg: U) => void) extends (arg: infer I) => void ? I : never
type Test = Broken<'foo' | 42>  // 'foo' | 42 — no distribution happened

The extends any wrapper is essential to distribute the union into individual function types first.

Practical use cases

Union to intersection is useful for merging configuration objects:

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

It’s also a building block for other advanced types like UnionToTuple.

Impossible intersections

When the union members are incompatible primitives, the intersection collapses to never:

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

This is expected — no value can be both string and number.

Key Takeaways