Skip to content
webtype.orgwebtype.org

    ↑↓ move · ⏎ open · esc close

    Concept 8 of 12

    Distributivity

    Distribution is the rule that surprises everyone once and then explains half the confusing behaviour in the type system. A conditional whose checked side is a bare type parameter does not run once — it runs once per union member, and the results are unioned back together.

    Follow the typeCompiler checked
    ExpressionEach<string>
    Resolves tostring[]
    ExpressionEach<number>
    Resolves tonever
    ExpressionEach<string | number>
    Resolves tostring[]
    ExpressionWhole<string | number>
    Resolves tonever

    Each checks union members separately, then combines the results; never drops out. Whole checks the entire union at once.

    Type definition
    type Each<T> = T extends string ? T[] : never
    type Whole<T> = [T] extends [string] ? T[] : never

    What "naked" means

    type Naked<T> = T extends string ? 'yes' : 'no'
    type A = Naked<string | number>   // 'yes' | 'no'  — ran twice
    
    type Wrapped<T> = [T] extends [string] ? 'yes' : 'no'
    type B = Wrapped<string | number>  // 'no' — ran once, on the whole union

    Naked means the parameter stands alone on the left of extends, not wrapped in a tuple, array, object or promise. Wrapping it in anything switches distribution off and compares the union as a single thing.

    Distribution is how filtering works

    type MyExclude<T, U> = T extends U ? never : T
    MyExclude<'a' | 'b' | 'c', 'a'>   // 'b' | 'c'
    
    // Each member becomes never or survives; unioning drops the nevers,
    // because never is the identity element of union.

    Exclude, Extract and NonNullable are all one distributive conditional. The trick is that never vanishes from a union, so mapping unwanted members to never removes them for free.

    Two idioms worth memorising

    // Force distribution, even when you do not need the test:
    type Each<K> = K extends K ? [K] : never
    
    // Suppress it, and detect the empty union:
    type IsNever<T> = [T] extends [never] ? true : false

    K extends K is trivially true and exists purely to distribute. [T] extends [never] is the only reliable way to ask "is this never", because a distributive conditional over never produces never and never reaches either branch.

    The common wrong answer

    // Intent: does T contain string?
    type HasString<T> = T extends string ? true : false
    
    HasString<string | number>  // boolean, not true
    // It distributed: true | false, which collapses to boolean.
    
    // Fixed: ask about the union as a whole.
    type HasString2<T> = [T] extends [string] ? true : false

    A conditional returning boolean instead of true or false is nearly always distribution you did not intend. The union of true and false is boolean, which is why the symptom looks so innocuous.

    Exercise

    Remove null from a union — and let the conditional distribute.

    Try it

    2
    • NoNull<string | null>—
      → string
    • NoNull<null>—
      → never

    Takeaway

    A bare parameter distributes; wrapping it in [] stops it. When a type mysteriously returns boolean or never, distribution is the first thing to check.

    Practiced in