Engineering Notes
Home/Languages/TypeScript/Mapped Types
Languages · TypeScript

Mapped Types

A mapped type builds a new object type by iterating over the keys of an existing type and transforming each property's type according to a rule — the type-level equivalent of Object.keys(obj).map(...).

💻 Languages● Intermediate⏱ 5 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • What a Mapped Type Is
  • Modifiers: readonly and ?
  • Removing Modifiers with -
  • Key Remapping with as
  • Filtering Keys Out with never

1What a Mapped Type Is

A mapped type builds a new object type by iterating over the keys of an existing type and transforming each property's type according to a rule — the type-level equivalent of Object.keys(obj).map(...).

ts
type Flags<T> = {
  [P in keyof T]: boolean;
};

interface FeatureSet { darkMode: string; betaAccess: number; }

type FeatureFlags = Flags<FeatureSet>;
// { darkMode: boolean; betaAccess: boolean } — every property's TYPE replaced, keys kept

keyof T produces a union of T's property names (its own topic); [P in <that union>] iterates each one, and whatever comes after the colon defines the new type for that key.

2Modifiers: readonly and ?

ts
type MyReadonly<T> = { readonly [P in keyof T]: T[P] };
type MyPartial<T> = { [P in keyof T]?: T[P] };

These are exactly how the real Readonly<T> and Partial<T> utility types (Utility Types topic) are implemented — mapped types with a modifier applied uniformly to every property.

3Removing Modifiers with -

ts
type MyRequired<T> = { [P in keyof T]-?: T[P] };  // REMOVES optionality
type MyMutable<T> = { -readonly [P in keyof T]: T[P] }; // REMOVES readonly

+ is also valid but is the default and rarely written explicitly (+? and ? mean the same thing) — - is the one you actually need, to strip a modifier that the source type already has.

4Key Remapping with as

ts
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface Person { name: string; age: number; }

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

The as clause lets a mapped type rename each key using a template literal type, not just transform the value — this combination (mapped type + template literal key remapping) is how many advanced "generate an API surface from a data shape" patterns are built.

5Filtering Keys Out with never

ts
type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

interface Mixed { name: string; age: number; active: boolean; }

type NonBoolean = OmitByType<Mixed, boolean>; // { name: string; age: number }

Mapping a key to never inside the as clause removes that key from the resulting type entirely — this is the standard technique for building conditional key-filtering utilities like this OmitByType example.

Under the HoodHomomorphic Mapped Types▾

"Homomorphic" means the mapped type preserves the source's structure automatically

A mapped type that iterates directly over keyof T (like every example above) is called homomorphic — it automatically carries over each property's existing modifiers (whether it was already readonly, already optional) from T, unless you explicitly add/remove them with +/-. This is why Partial<T> only needs to add ? — it doesn't need to separately preserve whatever readonly status each property already had; homomorphic mapping does that automatically as part of the mechanism.

ts
interface Config {
  readonly host: string;
  port?: number;
}

type PartialConfig = Partial<Config>;
// { readonly host?: string; port?: number }
//    ^ readonly PRESERVED automatically — Partial never mentioned readonly at all
Partial<Config> automatically keeps host's readonly modifier while adding optional to every property
Partial<Config> automatically keeps host's readonly modifier while adding optional to every property

Non-homomorphic mapped types don't get this — you're on your own

ts
type AlwaysThree<K extends string> = { [P in K]: number }; // K is not `keyof` anything specific

When the key union being mapped over doesn't come directly from keyof T (here, K is just any string union, not tied to a specific source type's keys), there's no original property to copy modifiers from — so the mapped type is built from scratch, with no automatic readonly/optional preservation, because there's nothing there to preserve.

Key remapping's as clause is evaluated per-key, and never is a real removal signal

For section 5's filtering pattern, the checker evaluates the conditional inside as once per key during iteration; when that conditional resolves to never for a given key, the checker's mapped-type construction logic treats a never-mapped key as absent from the resulting type — not present-with-type-never, genuinely removed. This is a deliberate, specified behavior of the key-remapping feature (not a side effect of never's general "no possible values" meaning) that makes conditional key filtering possible at all.

Mapped types over keyof T are why T[P] (indexed access) inside the mapping works

Inside a homomorphic mapped type, P is a genuine type variable ranging over T's actual key union — T[P] (an indexed access type, briefly touched on in the Keyof/Typeof topic) looks up each specific property's original type as the loop iterates, which is exactly the mechanism that lets Readonly<T>'s definition { readonly [P in keyof T]: T[P] } keep every property's original type while only changing its modifiers.

⚠️ Common Pitfalls

  1. Trying to iterate over a key union not derived from keyof T and expecting modifier preservation anyway — only homomorphic mapping (iterating keyof T directly) preserves modifiers automatically.
  2. Forgetting never in an as clause removes the key rather than including it with type never — an easy source of confusion when first learning key remapping.
  3. Using +readonly/+? when the intent was actually to remove a modifier — + is the (redundant) default; you almost always want - when the goal is stripping an existing modifier.
  4. Not realizing built-in utility types ARE mapped types — see the Utility Types topic's "Under the Hood" section; if a mapped type isn't behaving as expected, comparing it against Partial/Readonly's real definitions is often the fastest way to spot the issue.

📋 Quick Reference

text
{ [P in keyof T]: T[P] }              → homomorphic mapped type, preserves modifiers automatically
{ [P in keyof T]?: T[P] }               → adds optional
{ [P in keyof T]-readonly: T[P] }        → removes readonly
{ [P in keyof T as NewKey]: T[P] }        → key remapping
{ [P in keyof T as (cond ? never : P)]: T[P] } → conditional key removal

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What is a mapped type?core▾

A type that transforms each property of another: { [K in keyof T]: Transform<T[K]> }, with modifiers readonly, ?, and removal via -readonly/-?.

Follow-ups interviewers ask
  • Built-in examples? Partial, Readonly, Record.
Q2What is key remapping?core▾

Using as in mapped types to rename or filter keys: `{ [K in keyof T as get${Capitalize<string & K>}]: () => T[K] } generates getters; mapping to never` removes keys.

Follow-ups interviewers ask
  • Filter by value type? [K in keyof T as T[K] extends Function ? never : K].
Q3How would you make all nested properties optional?senior▾

A recursive mapped type: type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }, handling arrays and functions specially.

Follow-ups interviewers ask
  • Why special-case arrays? To keep array structure correct.
Q4How do mapped types help with forms or APIs?core▾

Deriving error maps ({ [K in keyof Form]?: string }), touched-state maps, or DTO variants from a single source type, keeping them in sync.

Follow-ups interviewers ask
  • Benefit? One source of truth.
Q5Are mapped types homomorphic?core▾

Mapping over keyof T preserves modifiers (optional/readonly) of the original properties; mapping over arbitrary keys doesn't.

Follow-ups interviewers ask
  • Why does it matter? Partial<T> keeps readonly flags, for example.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.