Mapped Types

advanced mapped-types advanced-types transformations

In simple language, a mapped type is a for loop over the keys of a type. We take an existing type, iterate over its keys, and produce a new type where each property has been transformed somehow.

The syntax looks weird at first: { [K in keyof T]: ... }. Read it as “for each key K in the keys of T, give me back a property with this new shape”. It’s the type-level equivalent of Object.fromEntries(Object.keys(obj).map(...)).

The basic shape

type User = { id: number; name: string; email: string };

// Make every property optional — this is basically how Partial<T> is built
type PartialUser = { [K in keyof User]?: User[K] };
// { id?: number; name?: string; email?: string }

Notice three things: K in keyof User walks the keys, ? adds optionality, and User[K] is indexed access to grab the original property type. Swap those modifiers around and we get a whole family of transforms.

{ [K in keyof T]: Transform<T[K]> }
Input
{ id: number;
  name: string }
Output (readonly)
{ readonly id: number;
  readonly name: string }

Mapping modifiers — ? and readonly

We can add or remove modifiers with + (default) or -. The - is the cool one because it strips modifiers that already exist.

type MakeOptional<T> = { [K in keyof T]?: T[K] };
type MakeRequired<T> = { [K in keyof T]-?: T[K] }; // strip optional
type Freeze<T> = { readonly [K in keyof T]: T[K] };
type Thaw<T> = { -readonly [K in keyof T]: T[K] };  // strip readonly

type LooseConfig = { host?: string; port?: number };
type StrictConfig = MakeRequired<LooseConfig>; // { host: string; port: number }

This is exactly how Partial, Required, Readonly are implemented in the standard lib — it’s just a thin wrapper around a mapped type.

Key remapping with as

TS 4.1+ lets us rename keys during mapping using as. Combine it with template literal types and we can build getter shapes, event handlers, anything name-based.

type User = { name: string; age: number };

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

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

The as clause runs per-key and decides the new key name. If we return never from the as, that key gets filtered out — which is how we exclude properties.

type RemoveKey<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

type WithoutEmail = RemoveKey<{ id: number; email: string }, "email">;
// { id: number }

Mapping a union of strings

K in doesn’t have to start from keyof — any union of keys works. Useful for building records out of thin air.

type Role = "admin" | "editor" | "viewer";

type RolePermissions = { [R in Role]: string[] };
// { admin: string[]; editor: string[]; viewer: string[] }

const perms: RolePermissions = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

This is basically Record<Role, string[]> written long-hand — and yes, Record itself is just a mapped type.

Real-world: building a form state type

Here’s the kind of pattern that shows up in actual code. Given a form’s value shape, derive matching error and touched maps automatically.

type FormValues = { email: string; password: string; remember: boolean };

type FormErrors<T> = { [K in keyof T]?: string };
type FormTouched<T> = { [K in keyof T]: boolean };

type LoginForm = {
  values: FormValues;
  errors: FormErrors<FormValues>;     // { email?: string; ... }
  touched: FormTouched<FormValues>;   // { email: boolean; ... }
};

If we add a new field to FormValues, the errors and touched types update for free. That’s the whole point of mapped types — write the shape once, derive everything else.