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

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

    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