Mapped Types
Reviewed & published by Brayan K
A mapped type uses the syntax { [K in keyof T]: ... } to loop over every key of an existing type and produce a new type where each property's value, optionality, readonly-ness, or even name is transformed — it is the type-level "for each property" that powers Partial, Readonly, and the rest.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Read and write the { [K in keyof T]: T[K] } mapped-type syntax
- Transform value types per key with lookup types T[K]
- Add and remove modifiers: readonly, ?, -readonly, -?
- Rename keys with key remapping using `as`
- Filter keys out by remapping them to never
- See how Partial, Readonly, Required and Record are built from mapped types
💡 Real-World Analogy
A mapped type is a stamping machine on an assembly line. The original type T is a tray of labelled parts. The machine picks up every part (K in keyof T), does the same operation to each — paint it (change the value type), mark it "do-not-touch" (readonly), make it optional (?), or relabel it (as) — and drops the transformed part into a new tray. You describe the operation once; the machine applies it uniformly to all keys, no matter how many there are.
1. The [K in keyof T] Mechanism
Every mapped type starts the same way: keyof T gives a union of T's property names, and [K in keyof T] iterates over each name K. For the value you almost always use the lookup type T[K] ("the type of property K"). Identity in, identity out — until you change something:
interface User { id: number; name: string; admin: boolean }
// Identity mapped type — same as User
type Same<T> = { [K in keyof T]: T[K] };
type U1 = Same<User>; // { id: number; name: string; admin: boolean }
// Change the VALUE type for every key -> "make everything a string"
type Stringify<T> = { [K in keyof T]: string };
type U2 = Stringify<User>; // { id: string; name: string; admin: string }
// Wrap each value -> a getter function per key
type Getters<T> = { [K in keyof T]: () => T[K] };
type U3 = Getters<User>;
// { id: () => number; name: () => string; admin: () => boolean }The runnable box performs the same "walk every key and rebuild" over a real object, the way the compiler does at the type level.
// === A mapped type rebuilds an object key-by-key. At runtime, the "rebuild" ===
// is a loop over the keys. Here's the runtime data a mapped type transforms.
const user = { id: 1, name: "Alice", admin: false };
// In TS: type Optional<T> = { [K in keyof T]?: T[K] } walks every key K.
// The runtime equivalent of "walk every key" is Object.keys + a loop:
const keys = Object.keys(user); // ["id", "name", "admin"]
console.log("keys:", keys); // keys: [ 'id', 'name', 'admin' ]
// "Map" each key to a NEW value — e.g. stringify every field.
// This mirrors { [K in keyof T]: string } at runtime:
const stringified = {};
for (const k of keys) {
stringified[k] = String(user[k]);
}
console.log("all strings:", stringified);
// all strings: { id: '1', name: 'Alice', admin: 'false' }
// "Map" each key to a getter function — mirrors { [K in keyof T]: () => T[K] }:
const getters = {};
for (const k of keys) {
getters[k] = () => user[k];
}
console.log("name via getter:", getters.name()); // name via getter: Alice2. Key Remapping with as
Since TypeScript 4.1 you can rename keys during the map using as, almost always with a template literal type. This is how you generate getX/setX method names from data fields. Remap a key to never and the property disappears — that is the standard way to filter keys.
// === Key remapping with "as" — rename keys while mapping ===
// In TS: { [K in keyof T as `get${Capitalize<K>}`]: () => T[K] }
// renames "name" -> "getName", etc. At runtime, renaming a key is just
// building a new object with computed key names.
const user = { name: "Alice", age: 30 };
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const withGetters = {};
for (const k of Object.keys(user)) {
const newKey = "get" + cap(k); // "name" -> "getName"
withGetters[newKey] = () => user[k];
}
console.log("new keys:", Object.keys(withGetters));
// new keys: [ 'getName', 'getAge' ]
console.log("getName():", withGetters.getName()); // getName(): Alice
console.log("getAge():", withGetters.getAge()); // getAge(): 30
// Key remapping can also FILTER keys out (map a key to `never` to drop it).
// Runtime equivalent: skip keys you don't want.
const record = { id: 1, name: "Bob", secret: "xyz" };
const safe = {};
for (const k of Object.keys(record)) {
if (k === "secret") continue; // "as never" drops this key
safe[k] = record[k];
}
console.log("safe:", safe); // safe: { id: 1, name: 'Bob' }3. Modifiers: readonly, ?, and Their Removers
A mapped type can add or strip the readonly and optional (?) modifiers on every property. Prefix a modifier with - to remove it. These four lines are essentially the entire source of TypeScript's Partial, Required, and Readonly:
interface User { readonly id: number; name?: string }
// Add optional to every key -> this IS Partial<T>
type Partial<T> = { [K in keyof T]?: T[K] };
// Remove optional from every key -> this IS Required<T>
type Required<T> = { [K in keyof T]-?: T[K] };
// Add readonly to every key -> this IS Readonly<T>
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Remove readonly from every key -> a "Mutable" helper
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// You can combine them — strip BOTH modifiers at once:
type Concrete<T> = { -readonly [K in keyof T]-?: T[K] };Modifiers are type-level, but the box below shows their runtime consequence: filling in defaults so no key is missing (the effect of -?) and freezing an object so it can't be reassigned (the effect of readonly).
// === Modifiers: ?, readonly, and the "remove" variants -? and -readonly ===
// A mapped type can ADD or REMOVE optionality and readonly-ness.
// These are compile-time only, but we can SEE the runtime effect:
// - making a field optional = it may be ABSENT from the object
// - "removing optional" (-?) = filling in defaults so it's always present
// Simulate Required<T> at runtime: ensure every key has a value (no holes).
function makeRequired(partial, defaults) {
return { ...defaults, ...partial }; // -? : fill missing keys from defaults
}
const partialConfig = { theme: "dark" }; // some keys missing
const defaults = { theme: "light", fontSize: 12, autosave: true };
const full = makeRequired(partialConfig, defaults);
console.log("full config:", full);
// full config: { theme: 'dark', fontSize: 12, autosave: true }
// Simulate Readonly<T> at runtime with Object.freeze — writes are ignored:
const frozen = Object.freeze({ ...full });
frozen.theme = "blue"; // silently ignored (strict mode: throws)
console.log("after write attempt:", frozen.theme); // after write attempt: dark
console.log("is frozen?", Object.isFrozen(frozen)); // is frozen? true🎯 Your Turn
Build the runtime version of a key-remapping mapped type that turns each field into a setX method. Fill in the blanks and match the expected output.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// Build the runtime version of a key-remapping mapped type:
// { [K in keyof T as `set${Capitalize<K>}`]: (v) => T }
const settings = { volume: 8, muted: false, label: "Main" };
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const setters = {};
for (const k of Object.keys(settings)) {
// 1) Build the remapped key name, e.g. "volume" -> "setVolume"
let newKey = ___; // 👉 "set" + cap(k)
// 2) Each setter writes the new value back, then returns the object
setters[newKey] = (v) => { settings[k] = v; return settings; };
}
console.log("setter keys:", Object.keys(setters));
// 3) Call the setter that changes volume to 10
___; // 👉 setters.setVolume(10)
console.log("after setVolume:", settings.volume);
// ✅ Expected output:
// setter keys: [ 'setVolume', 'setMuted', 'setLabel' ]
// after setVolume: 10Common Errors (and the fix)
- ❌ Using a key type that isn't a string in a template literal: `get${Capitalize<K>}` errors because K may be string | number | symbol. ✅ Constrain it: Capitalize<string & K> or K & string.
- ❌ Expecting a mapped type to run: there is no JavaScript output from { [K in keyof T]: ... } — it vanishes at compile time. ✅ To transform real data, write an actual loop (Object.keys + build a new object), as in the boxes here.
- ❌ Forgetting that readonly is shallow: Readonly<T> freezes the top-level keys only; nested objects are still mutable at the type level. ✅ Write a recursive DeepReadonly<T> if you need deep immutability.
- ❌ Mapping over a union instead of an object: { [K in keyof T]: ... } expects T to be an object type. ✅ If you want to iterate a string-literal union directly, write { [K in MyUnion]: V } (no keyof).
- ❌ Remapping to never by accident: a conditional in the as clause that always yields never produces an empty type {}. ✅ Double-check the branch — only the keys you want to drop should resolve to never.
Pro Tips
- 💡 Read a mapped type aloud: "for each key K in keyof T, the value is …". It demystifies even gnarly ones.
- 💡 Pair mapped types with conditional types in the value position to transform some keys differently from others.
- 💡 Use as never to filter — e.g. keep only function-valued properties, or strip all keys starting with _.
- 💡 Don't reinvent the wheel: if a built-in utility already does it (Partial, Readonly, Record), use that — they are mapped types written for you.
Mini-Challenge: Validators
Implement the runtime form of a key-remapping mapped type that produces a <key>Valid method for every field. Follow the outline, run it, and match the example output.
// 🎯 MINI-CHALLENGE: a "Validators" mapped type at runtime
// In TS this is a key-remapping mapped type:
// type Validators<T> = { [K in keyof T as `${string & K}Valid`]: () => boolean };
//
// 1. Start from a form object:
// { username: "alice", age: 30, email: "a@b.com" }
// 2. Write toValidators(obj) that, for EACH key, adds a new key "<key>Valid"
// whose value is a function returning whether obj[key] is non-empty
// (not "" and not null/undefined).
// 3. Log the new keys, then call usernameValid() and ageValid().
//
// ✅ Example output:
// keys: [ 'usernameValid', 'ageValid', 'emailValid' ]
// usernameValid: true
// ageValid: true
// your code here🎉 Lesson Complete
- ✅ { [K in keyof T]: T[K] } loops every key; T[K] is the per-key lookup type
- ✅ Replace or wrap T[K] to transform each property's value type
- ✅ Add/strip modifiers: readonly, ?, and the removers -readonly, -?
- ✅ Rename keys with as + template literal types; remap to never to drop a key
- ✅ Partial, Required, Readonly, Record are all one-line mapped types
- ✅ Mapped types are compile-time only — at runtime you'd loop the keys yourself
Practice quiz
What does the mapped type syntax { [K in keyof T]: ... } do?
- Loops over the values of T
- Deletes keys from T
- Loops over every key K of T to build a new type
- Runs a for loop at runtime
Answer: Loops over every key K of T to build a new type. It is a type-level loop: for each key K in keyof T, define a property.
Inside a mapped type, what is T[K] usually?
- The lookup type: the type of property K
- A runtime value
- Always string
- The number of keys
Answer: The lookup type: the type of property K. T[K] is a lookup type that keeps each property's original type.
What does the minus sign do in { [K in keyof T]-?: T[K] }?
- Adds optionality
- Subtracts a number
- Makes properties readonly
- Removes the optional modifier from every property
Answer: Removes the optional modifier from every property. Prefixing a modifier with - removes it; -? strips optional, which is how Required<T> works.
How is Partial<T> defined as a mapped type?
- { [P in keyof T]-?: T[P] }
- { [P in keyof T]?: T[P] }
- { readonly [P in keyof T]: T[P] }
- { [P in T]: never }
Answer: { [P in keyof T]?: T[P] }. Partial<T> adds the optional modifier: { [P in keyof T]?: T[P] }.
How do you rename keys during a mapped type?
- With an as clause, usually a template literal type
- With the rename keyword
- By assigning to keyof
- You cannot rename keys
Answer: With an as clause, usually a template literal type. Key remapping uses an as clause with a template literal type to compute each new key name.
How do you filter (drop) a key in a mapped type?
- Set its value to null
- Use delete
- Remap the key to never
- Set it to undefined
Answer: Remap the key to never. Remapping a key to never omits that property from the result.
Which built-in is a one-line mapped type?
- console.log
- Readonly<T>
- JSON.parse
- Math.max
Answer: Readonly<T>. Partial, Required, Readonly, and Record are all one-line mapped types in TypeScript's lib.
Why can a template literal key like the capitalized form of K error without constraining K?
- Capitalize is not real
- get is reserved
- Mapped types ban templates
- K may be string | number | symbol, but template literals need a string
Answer: K may be string | number | symbol, but template literals need a string. Constrain it with Capitalize<string & K> or K & string so the key is a string.
What is the runtime form of a mapped type?
- A special runtime object
- There is none; mapped types are compile-time only
- A frozen array
- A Proxy
Answer: There is none; mapped types are compile-time only. Mapped types vanish at compile time; to transform real data you write an actual loop.
What does { readonly [K in keyof T]: T[K] } produce?
- Partial<T>
- Required<T>
- Readonly<T>
- Record<K, V>
Answer: Readonly<T>. Adding readonly to every key is exactly how Readonly<T> is defined.
Continue this course
- Previous: Utility Types (Partial, Pick, Omit, Record)
- Next: Conditional Types & infer — T extends U ? X : Y, distribution, and extracting types with infer
- Quick reference: TypeScript cheat sheet
Frequently asked questions
What exactly does [K in keyof T] mean?
It is a loop at the type level. 'keyof T' produces a union of all of T's property names; '[K in ...]' iterates over each name K and lets you describe the value type for that key, usually with T[K] (a lookup type). So { [K in keyof T]: T[K] } reproduces T exactly, and you change the value type or add modifiers to transform it. Think of it as 'for each property K of T, define a property whose value is …'.
What do the - signs in -readonly and -? do?
By default a mapped type can ADD the readonly and optional (?) modifiers. Prefixing a modifier with a minus sign REMOVES it. So { -readonly [K in keyof T]-?: T[K] } strips both readonly and optional from every property — this is exactly how the built-in Required<T> and Mutable-style helpers are written. A plus sign (+readonly, +?) is allowed too but is the redundant default.
How does key remapping with 'as' work, and how do I drop a key?
Inside the mapped type you can rewrite the key: { [K in keyof T as NewKey]: ... }. NewKey is usually a template literal type that transforms K (e.g. `get${Capitalize<K & string>}`). If you remap a key to the type 'never', that property is OMITTED from the result — this is the standard trick for filtering keys out, for example dropping all function-valued properties or all keys that start with an underscore.
Are utility types like Partial and Readonly really just mapped types?
Yes. They are one-line mapped types in TypeScript's lib: Partial<T> is { [P in keyof T]?: T[P] }, Readonly<T> is { readonly [P in keyof T]: T[P] }, Required<T> is { [P in keyof T]-?: T[P] }, and Record<K, V> is { [P in K]: V }. Once you understand mapped types you understand how every property-transforming utility is built — and you can write your own.