Skip to content
webtype.orgwebtype.org

    ↑↓ move · ⏎ open · esc close

    Concept 4 of 12

    Mapped types

    A mapped type is a loop over a union of keys that builds an object. Partial, Required, Readonly and Pick are all one line of this, and once the shape is familiar you can write any of them from memory.

    Follow the typeCompiler checked
    ExpressionFlags<"read">
    Resolves to{ read: boolean }
    ExpressionFlags<"read" | "write">
    Resolves to{ read: boolean; write: boolean }

    Each member of the key union becomes one property. The value rule is applied to every key.

    Type definition
    type Flags<K extends string> = { [P in K]: boolean }

    The shape

    type MyPartial<T> = { [K in keyof T]?: T[K] }
    //                     ^^^^^^^^^^^^^^  ^  ^^^^
    //                     key set         |  value type
    //                                     modifier

    Read it as: for every K in this key set, produce a property named K whose type is that expression. The key set is any union of string, number or symbol — usually keyof T, sometimes a parameter, occasionally something computed.

    Modifiers add and subtract

    type Optional<T> = { [K in keyof T]+?: T[K] }   // add ?
    type Required<T> = { [K in keyof T]-?: T[K] }   // remove ?
    type Frozen<T> = { readonly [K in keyof T]: T[K] }
    type Thawed<T> = { -readonly [K in keyof T]: T[K] }

    Mapping over a bare keyof T is *homomorphic*, which means existing ? and readonly modifiers are copied across automatically. That is why an identity mapping changes nothing, and why removing a modifier needs an explicit minus.

    Renaming keys with as

    type Getters<T> = {
      [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
    }
    
    // A key renamed to never disappears — this is how you filter.
    type OnlyStrings<T> = {
      [K in keyof T as T[K] extends string ? K : never]: T[K]
    }

    The as clause rewrites each key as the mapping runs. A key rewritten to never cannot exist, so the property is dropped — which is the only way to remove a property from an object type.

    The common wrong answer

    // Intent: keep only the string-valued properties.
    type OnlyStrings<T> = {
      [K in keyof T]: T[K] extends string ? T[K] : never
    }
    // Every key survives; the rejected ones just hold never.
    // { name: string; age: never }

    Filtering on the value side cannot remove a key, only empty it. Filtering happens in the key clause with as ... : never, and mixing the two up produces a type that looks filtered and enforces nothing.

    Exercise

    Make every property of T a string, keeping the same keys.

    Try it

    1
    type User = { id: number; admin: boolean }
    • Stringify<User>—
      → { id: string; admin: string }

    Takeaway

    { [K in Keys]: Value } is the loop. Modifiers attach to the property, as rewrites the key, and never as a key is how things disappear.

    Practiced in