Distributive Conditional Types

Conditionals Distribute Over Unions

When the checked type is a naked type parameter and you pass a union, the conditional is applied to each member separately and the results are joined: F<A | B> = F<A> | F<B>. This is how Exclude and NonNullable work.

type MyExclude<T, U> = T extends U ? never : T;
// MyExclude<"a" | "b" | "c", "a">
//   = ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b") | ("c" extends "a" ? never : "c")
//   = never | "b" | "c"
//   = "b" | "c"

Seeing Distribution

Example

ToArray<string | number> distributes to string[] | number[]. Wrapping both sides in brackets, [T] extends [any], turns distribution off and gives (string | number)[]. The error message shows both results.

type ToArray<T> = T extends any ? T[] : never;
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;

const a: ToArray<string | number> = [1, "two"];            // error: mixed array not allowed
const b: ToArrayNoDistribute<string | number> = [1, "two"]; // OK

Compiler output (tsc)

Line 4: error TS2322: Type '(string | number)[]' is not assignable to type 'string[] | number[]'.
  Type '(string | number)[]' is not assignable to type 'string[]'.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

Filtering a Union of Objects

Example

type Event =
  | { type: "page_view"; path: string }
  | { type: "add_to_cart"; sku: string; qty: number }
  | { type: "purchase"; orderId: string; amount: number };

type EventOfType<E, K> = E extends { type: K } ? E : never;
type MoneyEvents = Event extends infer E ? (E extends { amount: number } ? E : never) : never;

const cartEvent: EventOfType<Event, "add_to_cart"> = { type: "add_to_cart", sku: "SKU-1", qty: 2 };
const money: MoneyEvents = { type: "purchase", orderId: "OD-1", amount: 999 };
console.log(cartEvent.qty, money.amount);

Output

2 999