Skip to content
C Codeloom
TypeScript

TypeScript Mapped Types Deep Dive

A practical tour of mapped types in TypeScript, including key remapping, modifiers, and patterns that build powerful generic utilities.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How mapped types transform object shapes
  • Using keyof, in, and as for key remapping
  • Applying readonly and optional modifiers
  • Composing mapped types with conditionals
  • Building reusable utility types

Prerequisites

  • Comfortable with JS
  • Basic TypeScript generics

What and Why

A mapped type walks over the keys of a type and produces a new type by transforming each property. They are the engine behind Partial, Required, Readonly, and Pick. Learning them is the difference between copy-pasting utility types and writing your own that fit your domain.

The reason mapped types are powerful is simple: instead of repeating the same change across many interfaces, you express the change once and apply it everywhere. That makes large codebases easier to evolve.

Mental Model

A mapped type looks like an object literal where the key is computed using in keyof. Think of it as a for loop at the type level.

for each K in keyof T:
 new property [K] = transform(T[K])

{ [K in keyof T]: NewType<T[K]> }
Mapped type as a type-level loop

You can change three things at each step: the key, the value, and the modifiers (readonly, ?).

Hands-on Example

Start with a User type and build several mapped utilities on top of it.

interface User {
  id: number;
  name: string;
  email: string;
  isAdmin: boolean;
}

A basic Partial lookalike:

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

type PartialUser = MyPartial<User>;
// { id?: number; name?: string; ... }

Stripping readonly with the - modifier:

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

Renaming keys using as. Here we prefix every key with db_:

type DbColumns<T> = {
  [K in keyof T as `db_${string & K}`]: T[K];
};

type UserColumns = DbColumns<User>;
// { db_id: number; db_name: string; db_email: string; db_isAdmin: boolean }

Filtering keys by value type. We keep only properties whose value extends string:

type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type UserStrings = StringKeys<User>;
// { name: string; email: string }
keyof User: id | name | email | isAdmin
for each K:
  if T[K] extends string -> keep K
  else                    -> never (drop)
result: name | email
How as remapping filters keys

Combining with conditional types, you can build a deep Readonly:

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

This walks every nested object and freezes it at the type level.

Common Pitfalls

Confusing keyof T with the values of T. keyof User gives "id" | "name" | "email" | "isAdmin", not the property values. When you index with T[K], you get the value type.

Using template literal keys with non-string keys. If K can be number or symbol, the template `${K}` will not work. Constrain with string & K or filter keys first.

Forgetting -? versus ?. The minus removes optionality; without it, you only add it. The same applies to readonly versus -readonly.

Applying mapped types to union types unexpectedly. A mapped type distributes over a naked type parameter if it appears in a conditional. To prevent distribution, wrap the parameter in a tuple.

Building mapped types on Record<string, unknown> and wondering why every key is allowed. Use a constrained generic like T extends object to keep inference sharp.

Best Practices

Name your utilities after intent, not mechanics. Nullable<T> reads better than MakeNullish<T>. The next person reading the code does not care how it works, only what it produces.

Lean on TypeScript’s existing utility types when they fit. Partial, Required, Readonly, Pick, Omit, and Record cover most everyday cases. Only build your own when you find yourself repeating a transformation.

Test types with expectTypeOf or tsd. Type tests are cheap and catch regressions when you refactor a mapped type that hundreds of call sites depend on.

Keep transformations small. If you need three modifiers and a rename, write three smaller mapped types and compose them. Easier to read, easier to debug.

Use infer within conditional types to extract pieces, then feed them into a mapped type. This combination unlocks expressing things like “make all functions on this type async.”

Wrap-up

Mapped types let you express transformations on whole shapes in a single line. With keyof, as, and the readonly/? modifiers, you can rename, filter, and modify properties to fit your domain. Use the built-in utilities first, then graduate to custom ones when patterns repeat. Master these, and your types start to feel like a small programming language working for you instead of fighting you.