Mapped types let one declaration produce whole families of related types, and they're how Partial, Readonly and Record work.
A mapped type creates an object type by looping over a union of keys:
type Flags<T> = { [K in keyof T]: boolean };For each key K in keyof T, the result has a property K of type boolean. The right-hand side can use K and T[K], so you can transform each property's type: { [K in keyof T]: () => T[K] } turns every property into a getter function.
Modifiers can be added or removed with + / -:
{ readonly [K in keyof T]: T[K] }— this isReadonly<T>.{ [K in keyof T]?: T[K] }—Partial<T>.{ -readonly [K in keyof T]-?: T[K] }— remove both, giving a mutableRequired.
When you map over keyof T for a type parameter T, the mapped type is homomorphic: it preserves the original optional and readonly modifiers, and it maps arrays and tuples to arrays and tuples instead of to objects with numeric keys.
Key remapping with as changes or filters the keys:
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type OnlyStrings<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };Mapping a key to never drops it — that's how you filter properties by their value type. (string & K is needed because keys may also be numbers or symbols, which Capitalize can't take.)
You can also map over any union, not just keyof: { [L in "en" | "fr"]: string } is essentially Record<"en" | "fr", string>.
Mapped types read best when named and kept small; compose several simple ones rather than writing one giant expression.
Example
interface Settings {
readonly theme: "light" | "dark";
fontSize?: number;
language: string;
}
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Validators<T> = { [K in keyof T]-?: (value: T[K]) => boolean };
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
type StringProps<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K] };
const validators: Validators<Settings> = {
theme: v => v === "light" || v === "dark",
fontSize: v => v === undefined || v > 6,
language: v => v.length === 2,
};
const s: Mutable<Settings> = { theme: "dark", language: "en" };
s.theme = "light"; // allowed: readonly removed
const getters: Getters<Settings> = {
getTheme: () => s.theme, getFontSize: () => s.fontSize, getLanguage: () => s.language,
};
type Strs = StringProps<Settings>; // { readonly theme: ...; language: string }
const strs: Strs = { theme: "dark", language: "de" };
console.log(validators.language("en"), getters.getTheme(), strs.language);Remember
{ [K in keyof T]: ... }transforms every property of T+/-add or removereadonlyand?asremaps keys; mapping a key toneverremoves it- Homomorphic maps keep modifiers and array/tuple shapes
Watch out for
- Forgetting
string & Kwhen building template-literal keys, since keys can also be number or symbol.