Mapped Types

A mapped type builds a new object type by looping over the keys of another type and transforming each property. It's how Partial, Readonly and Record are implemented — and once you understand the syntax, you can write your own.

The basic syntax

A mapped type looks like an index signature with in: for each key K in some union of keys, produce a property:

TS basic.ts
type Flags<Keys extends string> = {
  [K in Keys]: boolean;
};

type Features = Flags<"darkMode" | "betaSearch" | "newCheckout">;

const features: Features = {
  darkMode: true,
  betaSearch: false,
  newCheckout: true,
};

console.log(Object.entries(features).filter(([, on]) => on).map(([name]) => name));
Output
[ 'darkMode', 'newCheckout' ]

Read [K in Keys]: boolean as "for every K in Keys, a property named K of type boolean". That's exactly how the built-in Record<K, V> is defined.

Mapping over another type's keys

Combine in with keyof to transform every property of an existing type. T[K] refers to the original property's type:

TS over-keys.ts
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type Stringify<T> = {
  [K in keyof T]: string;
};

interface Point { x: number; y: number }

const unknownPos: Nullable<Point> = { x: 10, y: null };
const formatted: Stringify<Point> = { x: "10px", y: "4px" };
console.log(unknownPos, formatted);
Output
{ x: 10, y: null } { x: '10px', y: '4px' }

Adding and removing modifiers

A mapped type can add readonly or ? to every property — or remove them with a - prefix:

TS modifiers.ts
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };

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

const c: Mutable<Config> = { host: "a" };
c.host = "b";

const full: MyRequired<Config> = { host: "x" };
tsc output
error TS2741: Property 'port' is missing in type '{ host: string; }' but required in type 'MyRequired<Config>'.

MyPartial and MyReadonly are essentially how the built-in versions are written. -readonly and -? remove those modifiers, which is how Required works.

Renaming keys with as

Since TypeScript 4.1, the as clause lets you compute a new key name for each property, often with a template literal type:

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

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

const p: Getters<Person> = {
  getName: () => "Lin",
  getAge: () => 40,
};

console.log(p.getName(), p.getAge());
Output
Lin 40

string & K narrows the key to strings (keys can also be numbers or symbols), and Capitalize upper-cases the first letter.

Filtering properties

If the as clause produces never, that property is dropped. Combined with a conditional type (next lesson), this filters properties by their type:

TS filter.ts
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Product {
  id: number;
  name: string;
  sku: string;
  inStock: boolean;
}

const searchable: OnlyStrings<Product> = { name: "Lamp", sku: "L-01" };
console.log(Object.keys(searchable));
Output
[ 'name', 'sku' ]

A practical example: form state

Mapped types shine when one shape needs several "parallel" shapes — like a form that tracks a value, an error and a touched flag per field:

TS form.ts
interface SignupForm {
  email: string;
  password: string;
  age: number;
}

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

const errors: Errors<SignupForm> = { password: "Too short" };
const touched: Touched<SignupForm> = { email: true, password: true, age: false };

for (const field of Object.keys(touched) as (keyof SignupForm)[]) {
  if (touched[field] && errors[field]) console.log(`${field}: ${errors[field]}`);
}
Output
password: Too short

Add a field to SignupForm and both Errors and Touched pick it up automatically — and touched will fail to compile until you include it.

Try it yourself

Try it yourself
Console output