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

💡 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: Alice

2. 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: 10

Common Errors (and the fix)

Pro Tips

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

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

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.