Skip to content
webtype.orgTwelve concepts · 5/12

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.

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