Union to Intersection
Union to Intersection
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 & trueSolution
type UnionToIntersection<U> =
(U extends any ? (arg: U) => void : never) extends (arg: infer I) => void
? I
: neverThis 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 : neverWhen 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 : neverNow 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:
| Position | Variance | Multiple candidates merge via |
|---|---|---|
| Return type | Covariant | Union (|) |
| Parameter | Contravariant | Intersection (&) |
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 happenedThe 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 → neverThis is expected — no value can be both string and number.
Key Takeaways
- Distributive conditional types (
T extends any ? ... : never) process each union member independently - Contravariant inference in function parameters merges multiple
infercandidates via intersection (&) - Covariant inference in return types merges via union (
|) — the opposite effect - This pattern is a fundamental building block for many advanced TypeScript type utilities
- Understanding variance (covariant vs. contravariant positions) is essential for hard-level type challenges
