Skip to content
webtype.orgTwelve concepts · 4/12

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.

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