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

    ↑↓ move · ⏎ open · esc close

    Concept 5 of 12

    Modifiers

    Modifiers are the small print of mapped types. They are easy to write and easy to get subtly wrong, because the default behaviour — silently copying whatever was already there — looks identical to doing nothing.

    Homomorphic mappings inherit modifiers

    interface Draft { title?: string; readonly id: number }
    
    // Mapping over bare `keyof T` preserves ? and readonly:
    type Copy<T> = { [K in keyof T]: T[K] }
    type Same = Copy<Draft>   // { title?: string; readonly id: number }
    
    // Mapping over a computed key set does NOT:
    type Flat<T> = { [K in keyof T | never]: T[K] }
    type Bare = Flat<Draft>   // { title: string; id: number }

    A mapping is homomorphic only when the key set is exactly `keyof T` for a type parameter `T`. Compute the key set any other way — union it, filter it, rename with `as` — and every modifier is dropped by default.

    This catches people out when writing a `Merge` type: the result loses optionality from both sources, and nothing warns you.

    `-?` removes undefined too

    type T = { a?: string }        // a?: string | undefined
    type R = { [K in keyof T]-?: T[K] }
    // { a: string }  — not { a: string | undefined }

    Subtracting the optional modifier does two things at once: the key becomes required, and `undefined` is stripped from the value type. That second part is easy to forget and is usually what you wanted anyway.

    The common wrong answer

    // Intent: make everything optional.
    type Loose<T> = { [K in keyof T]: T[K] | undefined }
    
    // The key is still required — you must write { a: undefined }
    // rather than omitting it. Optional and "may be undefined"
    // are genuinely different properties.

    Widening the value is not the same as making the key optional, and exact-equality checks can tell them apart. If you meant optional, the `?` belongs after the key clause.

    Exercise

    Remove `readonly` from every property, and leave `?` alone.

    Try it

    1
    type Frozen = { readonly a: string; readonly b?: number }
    • Thaw<Frozen>
      { a: string; b?: number }

    Takeaway

    Modifiers act on the property, never the value. And a mapping that touches the key set at all forgets every modifier it was not told to keep.

    Practiced in