Skip to content
webtype.orgwebtype.org

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

    Follow the typeCompiler checked
    ExpressionDraft
    Resolves to{ readonly name?: string }
    ExpressionReady<Draft>
    Resolves to{ name: string }

    Remove readonly and optionality from each property. Here the resulting name is required and writable.

    Type definition
    type Draft = { readonly name?: string }
    type Ready<T> = { -readonly [K in keyof T]-?: T[K] }

    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