Careeroza
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"]; // OKCompiler 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