TypeScript by Example

Mapped Types

Build new types by transforming every key of an existing type - the mechanism behind Partial, Readonly, and the rest of the TypeScript stdlib.

Mapped types iterate over the keys of an existing type and produce a new type with transformed values or modifiers. They are how TypeScript's standard utility types (Partial, Readonly, Required) are implemented - not compiler magic, but plain TypeScript you can write yourself.

The { [K in keyof T]: ... } syntax creates a new type with the same keys as T but with transformed values. keyof T produces a union of all key names; K is the loop variable; the value on the right determines the new property type.

type User = {
  id: string;
  name: string;
  email: string;
};
 
// Every value becomes an array of that type
type Listify<T> = {
  [K in keyof T]: T[K][];
};
 
type UserLists = Listify<User>;
// { id: string[]; name: string[]; email: string[] }
 
// Wrap every value in a Promise
type Asyncify<T> = {
  [K in keyof T]: Promise<T[K]>;
};
 
type AsyncUser = Asyncify<User>;
// { id: Promise<string>; name: Promise<string>; email: Promise<string> }

Modifier prefixes control readonly and optionality. + adds the modifier (the default when you omit it); - removes it. This lets you strip all readonly constraints or make every optional property required.

type Config = {
  readonly host: string;
  readonly port: number;
  debug?: boolean;
};
 
// -readonly: strip all readonly modifiers
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};
 
type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }
 
// -?: make every optional property required
type Complete<T> = {
  [K in keyof T]-?: T[K];
};
 
type RequiredConfig = Complete<Config>;
// { readonly host: string; readonly port: number; debug: boolean }

Key remapping via as lets you rename keys as part of the mapped type. Combine with template literal types to derive new names - for example, converting every field into a getter method signature.

type Model = {
  id: string;
  name: string;
  createdAt: Date;
};
 
// Remap key names: prepend "get" and capitalize
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
type ModelGetters = Getters<Model>;
// {
//   getId:        () => string;
//   getName:      () => string;
//   getCreatedAt: () => Date;
// }
 
// Filter keys with `as` + `never` - omit all Function-valued properties
type DataOnly<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

A practical example: Versioned<T> adds a _version field to any entity for optimistic-locking. Enforcing it at the type level means the version field is present everywhere it's required - without repeating it in every entity definition.

type Versioned<T> = T & { _version: number };
 
type Product = { id: string; name: string; price: number };
type Order   = { id: string; items: string[]; total: number };
 
type VersionedProduct = Versioned<Product>;
type VersionedOrder   = Versioned<Order>;
 
function update<T extends { id: string }>(
  entity: Versioned<T>,
  patch: Partial<T>
): Versioned<T> {
  return { ...entity, ...patch, _version: entity._version + 1 };
}
 
const product: VersionedProduct = {
  id: "p1", name: "Widget", price: 9.99, _version: 1,
};
const updated = update(product, { price: 12.99 });
console.log(updated._version); // 2

In production

Readonly<T> and Partial<T> are stdlib mapped types - not compiler magic. Writing your own lets you enforce constraints at the type level without runtime overhead. A Versioned<T> that adds _version to any entity ensures the optimistic-lock field is required at every persistence boundary. Key remapping with as combined with never is the idiomatic way to filter keys - producing a narrowed type that only exposes the fields you want (e.g., stripping all Function properties for a serializable snapshot). If you find yourself writing the same optional/required/readonly transformation more than once, write a generic mapped type once and reuse it.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free