Skip to content
webtype.orgwebtype.orgTwelve concepts · 8/12

    ↑↓ 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.

    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