Skip to main content

What are mapped types in TypeScript

Mapped types are a TypeScript feature that creates a new type by iterating over every key of an existing type and applying a transformation to its value type.

Theory

TL;DR

  • Like Array.prototype.map() but for object types: input {name: string, age: number}, output {name: boolean, age: boolean} by changing each value type
  • Core syntax: { [K in keyof T]: NewValueType } iterates over all keys of T
  • Add ? to make optional, readonly to freeze, -readonly or -? to remove those modifiers
  • Key remapping with as clause (TypeScript 4.1+): filter or rename keys with template literals
  • Reach for mapped types when built-in utilities like Partial<T> or Pick<T, K> do not cover your transformation

Quick example

typescript
type User = { name: string; age: number; }; // Map each property to boolean type BooleanUser = { [K in keyof User]: boolean; }; // Result: { name: boolean; age: boolean } const perms: BooleanUser = { name: true, age: false }; // valid

The [K in keyof User] syntax iterates over the key union of User and applies boolean as the value type for each key. TypeScript generates the new object shape at compile time. Nothing runs at runtime.

Key difference

Mapped types transform every property systematically. A plain type alias just renames a structure without touching it. An intersection type adds properties on top. Mapped types react to the source: add a field to User and BooleanUser picks it up automatically. That reactivity is what makes them worth using in generic utilities.

Modifier syntax

Four patterns cover most cases:

  • [K in keyof T]?: T[K] makes all properties optional, same behavior as Partial<T>
  • readonly [K in keyof T]: T[K] makes all properties readonly, same as Readonly<T>
  • [K in keyof T]-?: T[K] removes optional from every property, same as Required<T>
  • -readonly [K in keyof T]: T[K] removes readonly, useful as a Mutable<T> helper

The - prefix is the TypeScript way to strip a modifier. You can mix modifiers with value transformations in the same mapping.

Key remapping with as (TypeScript 4.1+)

Key remapping lets you rename or filter keys during the mapping:

typescript
type EventHandlers<T> = { [K in keyof T as K extends `on${string}` ? K : never]: T[K]; }; interface ButtonProps { onClick: () => void; label: string; disabled?: boolean; } type Handlers = EventHandlers<ButtonProps>; // Result: { onClick: () => void }

The as clause runs a conditional on each key. Return never to drop the key from the output. Return a template literal type to rename it. This pattern appears often in component libraries that need to separate event callbacks from DOM attributes.

Homomorphic mapping

A mapped type is homomorphic when it maps directly over keyof T. In that case TypeScript preserves the original readonly and ? modifiers from the source type, unless you override them explicitly.

typescript
type User = { readonly id: string; name?: string }; type Copy = { [K in keyof User]: User[K] }; // Result: { readonly id: string; name?: string } - modifiers preserved

Mappings over arbitrary unions like [K in string] are not homomorphic and do not inherit modifiers. That is a common source of confusion when working with index signatures.

How TypeScript processes mapped types

The compiler expands a mapped type during type checking by substituting each key from the union into the mapping clause. For [K in keyof T] it walks the key union of T and generates a fresh object type literal per substitution. No runtime code is produced. Mapped types are erased like all other type annotations, and the expansion is cached per unique instantiation.

Common mistakes

Mistake 1: expecting index signatures to survive mapping

typescript
type Obj = { [key: string]: number }; type Bad = { [K in keyof Obj]?: Obj[K] }; // keyof Obj resolves to string | number // but the index signature itself is gone from the result

Fix: intersect the mapped result with the original index signature:

typescript
type Good = { [K in keyof Obj]?: Obj[K] } & { [key: string]: number | undefined };

Mistake 2: accidentally wiping readonly from the source

typescript
type RUser = { readonly id: string }; type Bad = { [K in keyof RUser]: string }; // id: string - readonly is gone because the explicit value type overrides it

To strip readonly intentionally use -readonly. To keep it, use RUser[K] as the value type and let homomorphic preservation do the work.

Mistake 3: nested mappings without recursion

typescript
// Breaks on nested objects - T[K] is not recursively transformed type ShallowPartial<T> = { [K in keyof T]?: Partial<T[K]> };

Fix with a recursive conditional type:

typescript
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;

Mistake 4: trying to rename keys without the as clause

typescript
// Cannot rename keys here - this only changes value types type Bad<T> = { [K in keyof T]: string }; // Correct: use `as` with Capitalize or template literals type Good<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: T[K] };

Real-world usage

  • TypeScript stdlib: Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, and Record<K, V> are all mapped types under the hood
  • React: React.ComponentProps<typeof Button> uses mapped types to extract HTML attribute types from a component
  • TanStack Query: UseQueryOptions maps keys to optional via { [K in keyof T]?: T[K] }
  • tRPC: maps procedure input and output shapes with per-endpoint modifiers
  • Zod: .partial() on a schema uses the same ? modifier pattern internally

Follow-up questions

Q: What is the difference between mapped types and conditional types?
A: Mapped types iterate over keys to produce an object shape. Conditional types (T extends U ? A : B) branch on type relationships. They work well together: { [K in keyof T]: T[K] extends string ? Uppercase<T[K]> : T[K] } uses both in one expression.

Q: What is a homomorphic mapped type?
A: A mapping that operates directly on keyof T. It inherits the original readonly and ? modifiers from T unless explicitly overridden. Mappings over arbitrary unions like [K in string] are not homomorphic and do not preserve modifiers.

Q: How do you build a deep Partial?
A: Use a recursive conditional type: type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;. The conditional stops recursion at primitive values.

Q: Can you remap keys with template literal types?
A: Yes, from TypeScript 4.1. The as clause accepts any type expression that resolves to string | number | symbol | never, including template literal types like `on${Capitalize<string & K>}`.

Q: Implement Diff<T, U> that removes keys present in U from T, preserving original modifiers.
A: type Diff<T, U extends keyof any> = { [K in keyof T as K extends U ? never : K]: T[K] };. Key remapping with a conditional maps excluded keys to never, which TypeScript drops from the output. Because it maps over keyof T directly it stays homomorphic and preserves modifiers.

Examples

Basic: transforming value types

typescript
type Status = { isActive: boolean; isAdmin: boolean; isVerified: boolean; }; // Replace all boolean values with string labels type StatusLabels = { [K in keyof Status]: string; }; // Result: { isActive: string; isAdmin: string; isVerified: string }

The shape of StatusLabels mirrors Status exactly, but every value type is replaced. Adding a new field to Status automatically adds it to StatusLabels.

Intermediate: extracting React event handlers

typescript
interface FormProps { onSubmit: (e: Event) => void; onChange: (value: string) => void; placeholder: string; disabled?: boolean; } // Keep only keys that start with "on" type HandlerProps<T> = { [K in keyof T as K extends `on${string}` ? K : never]: T[K]; }; type FormHandlers = HandlerProps<FormProps>; // Result: { onSubmit: (e: Event) => void; onChange: (value: string) => void }

The as K extends \on${string}` ? K : neverclause filters the key set. Any key that does not match the pattern is mapped toneverand dropped. This is the same mechanism behindReact.ComponentProps`.

Senior: key renaming with conditional exclusion

typescript
type User = { name: string; age: number; readonly id: string; }; // Rename `age` to `viewAge`, drop `id` entirely type SecureUser = { readonly [K in keyof User as K extends 'id' ? never : K extends 'age' ? 'viewAge' : K ]: User[K]; }; // Result: { readonly name: string; readonly viewAge: number }

The readonly modifier on the mapping applies to all output keys. The original id field disappears because its key maps to never. I have seen this pattern in API response transformers where server field names need to change before they reach UI components, and doing it at the type level catches mismatches at compile time.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?