Skip to content
ByteAtATime

Explaining the UnionToIntersection type

Published: at 12:00 AM

When working with TypeScript’s type system, we occasionally encounter problems that require sophisticated type manipulations. Recently, while building another project (more on that in another post soon!), I stumbled upon a fascinating utility type: UnionToIntersection. This seemingly obscure type transformation unveils several powerful concepts in TypeScript’s type system. Let’s break it down piece by piece.

The Problem: From Union to Intersection

Before diving into the solution, let’s clarify what unions and intersections are in TypeScript:

  • A union type (A | B) represents a value that can be either type A or type B
  • An intersection type (A & B) represents a value that has all properties of both type A and type B

Converting from a union to an intersection may seem counterintuitive, but it’s incredibly useful in certain scenarios, like merging function overloads or creating exhaustive object types.

Here’s the utility type we’ll explore, taken from this StackOverflow answer:

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

This compact yet powerful type transformation harnesses several TypeScript features that deserve our attention.

Distributive Conditional Types

At the core of our utility type is the concept of distributive conditional types, which is the first piece of the puzzle.

What Are Distributive Conditional Types?

A conditional type uses a syntax similar to JavaScript’s ternary operator: T extends A ? X : Y. It evaluates to X if type T is assignable to type A, and Y otherwise.

What makes conditional types really powerful is their distributive property. When T is a union type, TypeScript applies the conditional individually to each member of the union.

How Distribution Works

Let’s say we have a union type T = B | C. When we apply a conditional type:

T extends A ? X : Y

TypeScript transforms this to:

(B extends A ? X : Y) | (C extends A ? X : Y)

This distribution happens automatically with naked type parameters in conditional types.

Practical Application: Filtering Types

Among others, this distributive behavior is perfect for filtering types from unions. For example:

type NonNullable<T> = T extends null | undefined ? never : T;

type Entity = User | Group | null | undefined;
type NonNullEntity = NonNullable<Entity>; // User | Group

Here, NonNullEntity expands to:

type NonNullEntity =
  | (User extends null | undefined ? never : User)
  | (Group extends null | undefined ? never : Group)
  | (null extends null | undefined ? never : null)
  | (undefined extends null | undefined ? never : undefined);

type NonNullEntity = User | Group | never | never; // collapses to User | Group

What does it do?

In our case, the distributive behavior of the conditional type is used to transform a union into an intersection. As an example, let’s say we have U = A | B. When we apply a conditional type:

type DistributiveConditional<U> = U extends any ? (x: U) => void : never;

It gets transformed into:

type Distributed<U> =
  | (A extends any ? (x: A) => void : never)
  | (B extends any ? (x: B) => void : never);

Now, because every type extends any, this gets simplified to:

(x: A) => void | (x: B) => void

Contravariance

The most fascinating aspect of our utility type is how it leverages function parameter contravariance - a property that reverses the direction of type relationships in specific contexts.

What is Contravariance?

In TypeScript, function parameter types are contravariant. This means that when assigning functions, the parameter types can be more general but not more specific.

For example, a function that accepts Animal can be assigned to a variable expecting a function that accepts Dog (since any Dog handler can handle an Animal), but not vice versa.

How Contravariance Transforms Unions to Intersections

Here’s where things get interesting. Due to the contravariant nature of function parameters:

  • A function type (x: A | B) => void is assignable to both (x: A) => void and (x: B) => void
  • A function type that’s assignable to both (x: A) => void and (x: B) => void must be of the form (x: A & B) => void

This is precisely what our utility type exploits! Note how we get (x: A) => void | (x: B) => void from the distributive behavior of the conditional type. TypeScript then needs to find some type, I, that satisfies both (x: A) => void and (x: B) => void, which is exactly the intersection of A and B.

Putting It All Together

Now let’s analyze our utility type step by step, taking U = A | B as an example:

type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
  x: infer I,
) => void
  ? I
  : never;
  1. U extends any ? (x: U) => void : never - This distributes over the union, creating a union of function types
(A extends any ? (x: A) => void : never) | (B extends any ? (x: B) => void : never)
  1. This union of function types gets compared to (x: infer I) => void
(x: A) => void | (x: B) => void
  1. Due to contravariance, the only way this assignment can succeed is if I is the intersection of all union members
(x: A & B) => void
  1. We extract and return this intersection type I!

Conclusion

TypeScript’s type system is remarkably powerful, often in subtle and unexpected ways. If you’re interested in learning more, I would highly recommend browsing StackOverflow answers by the user jcalz, who has a knack for creating elegant and practical type utilities.