Mapped Types and Key Remapping
Transforming every property of a type at once: modifiers, adding and removing optionality, and remapping keys with as to build derived shapes.
Transforming every property of a type at once: modifiers, adding and removing optionality, and remapping keys with as to build derived shapes.
You need a type where every property of Photo is wrapped in a
validation result. Writing it out means restating the field list
a fourth time, and the fourth copy will drift from the first
three.
Partial, Readonly and Record are all built from one
construct that transforms every property of a type at once. By
the end of this lesson you will write your own, rename keys
while mapping, and combine mapping with the conditionals from
the previous lesson — which is where most of the interesting
types in real codebases come from.
type Photo = { id: string; name: string; size: number };
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type A = Nullable<Photo>;
// { id: string | null; name: string | null; size: number | null }[K in keyof T] iterates over the property names. T[K] is the
type of that property. The body is what each becomes.
That is the whole mechanism, and the built-ins are one line each:
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Record<K extends PropertyKey, V> = { [P in K]: V };The modifiers are the second thing to know. ? adds
optionality, -? removes it, readonly adds immutability, and
-readonly removes it:
type Mutable<T> = { -readonly [K in keyof T]: T[K] };+ is allowed and implied, so +? and ? are the same.
Mapping does not have to start from keyof T. Any union of keys
works:
type Flags<K extends string> = { [P in K]: boolean };
type A = Flags<"dryRun" | "verbose">;
// { dryRun: boolean; verbose: boolean }as inside the brackets remaps each key:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type A = Getters<Photo>;
// { getId: () => string; getName: () => string; getSize: () => number }Two things are happening. The template literal builds a new name
— covered properly in the next lesson — and Capitalize is one
of four built-in string transformations (Uppercase,
Lowercase, Capitalize, Uncapitalize).
string & K is there because keyof T can include number and
symbol, and a template literal needs a string. That
intersection narrows it.
Remapping to never removes the key, which is how you
filter:
type OmitByType<T, V> = {
[K in keyof T as T[K] extends V ? never : K]: T[K];
};
type A = OmitByType<Photo, number>;
// { id: string; name: string }That is a conditional inside a key position — the two mechanisms
from these two lessons doing one job. It is worth reading twice:
for each key, if its value type is V, produce never as the
key, which drops it.
There is a rule about which mapped types preserve modifiers, and it explains a real bug.
Bad — a mapping that silently
discards readonly and ?.
Good — a direct mapping, which preserves them.
readonly and ? come along.
A mapping of exactly this form copies the modifiers from the source unless you explicitly change them.
It also distributes over unions and preserves arrays and
tuples, which is why Partial<string[]> behaves sensibly.
Modifiers are dropped.
Remap with as, or map over something that is not keyof T,
and the property is lost.
The result looks like a no-op and quietly makes every optional field required and every readonly field mutable.
The distribution, concretely:
The pattern that produces most useful derived types: map to produce a union of keys, then index to collapse it.
Read it in two steps. The mapped type produces
{ id: "id"; name: "name"; size: never }. Indexing with
[keyof T] takes the union of all its values —
"id" | "name" | never — and never disappears.
The -? matters: an optional property makes its value type
include undefined, which would fail the extends check for
what is otherwise a matching key.
From that, a genuinely useful pair:
And a deep transformation, which needs recursion:
Arrays first, because an array is an object and the object branch would map its numeric indices and its methods. That ordering catches everyone once.
Four that earn their place in most codebases:
Expand is the one to remember. A derived type displays as its
expression — Partial<Omit<Photo, "id">> — and wrapping it
forces the checker to compute the shape, so hovers and error
messages show { name?: string; size?: number } instead.
Apply it at public boundaries, where the reader has not seen the definition.
You can now transform every property of a type, rename or drop
keys while doing it, and combine mapping with conditionals to
derive types from other types. The homomorphic rule is the one
to carry: a mapping that adds as loses readonly and ?, and
nothing warns you.
Next is Template Literal Types, which you have just used without a proper introduction. Types built from string patterns turn out to make a class of string-based API checkable rather than merely documented.
Before you move on, write KeysMatching<T, V> yourself and use
it to get the string keys of a type you have. Then remove the
-? and see what changes when a property is optional. That
detail is the difference between a helper that works and one
that quietly misses fields.
THE FORM
type X<T> = { [K in keyof T]: T[K] };
K iterates the keys; T[K] is the property type
MODIFIERS
? add optional -? remove it
readonly add readonly -readonly remove it
Partial<T> = { [K in keyof T]?: T[K] }
Required<T> = { [K in keyof T]-?: T[K] }
Readonly<T> = { readonly [K in keyof T]: T[K] }
Record<K,V> = { [P in K]: V }
KEY REMAPPING
[K in keyof T as NewName]: T[K]
Uppercase / Lowercase / Capitalize / Uncapitalize
`string & K` because keyof can include number and symbol
remap to `never` to REMOVE a key - that is how you filter
HOMOMORPHIC - the rule that bites
exactly [K in keyof T] preserves readonly and ?
ANY remap with `as`, or mapping a non-keyof union, LOSES them
a "no-op" wrapper silently makes everything required and mutable
homomorphic mapping also distributes over unions and keeps
arrays as arrays
MAP THEN INDEX - the key-filtering idiom
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T];
the mapped type makes { id: "id"; size: never }
[keyof T] takes the union of values; never disappears
-? matters: optional adds undefined and fails the check
RECURSION
arrays BEFORE object - an array is an object
deep types produce enormous error messages; use sparingly
Expand<T> = { [K in keyof T]: T[K] } & {}
forces evaluation so hovers and errors show the SHAPEtype Wrap<T> = {
[K in keyof T as K]: T[K]; // remapped
};
type Photo = { readonly id: string; caption?: string };
type A = Wrap<Photo>;
// { id: string; caption: string } - both modifiers gonetype Wrap<T> = {
[K in keyof T]: T[K];
};
type A = Wrap<Photo>;
// { readonly id: string; caption?: string } - preservedtype A = Partial<string[]>; // (string | undefined)[]
type B = Partial<Photo | Album>; // Partial<Photo> | Partial<Album>type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
type StringKeys = KeysMatching<Photo, string>; // "id" | "name"type PickByType<T, V> = Pick<T, KeysMatching<T, V>>;
type OmitByType<T, V> = Omit<T, KeysMatching<T, V>>;type DeepReadonly<T> = T extends (infer E)[]
? readonly DeepReadonly<E>[]
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;// Mutually exclusive fields
type OneOf<T, K extends keyof T> = Omit<T, K> &
{
[P in K]: Required<Pick<T, P>> &
Partial<Record<Exclude<K, P>, never>>;
}[K];
// A change set: every field optional, but no unknown fields
type Patch<T> = { [K in keyof T]?: T[K] };
// Rename every key with a prefix
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
// Force evaluation, for readable hovers and errors
type Expand<T> = { [K in keyof T]: T[K] } & {};