Mapped types

Builds a new object type by transforming every property of an existing one.

Signature
{ [Property in keyof Type]: NewType }
{ [Property in Union]: NewType }
  • Added in TypeScript 2.1

Parameters

ParameterTypeDescription
$Propertytype variable nameThe name bound to each key as the mapping iterates. Any identifier works; keyof Type is the usual source.
$UniontypeThe union of keys to iterate - keyof Type, a string literal union, or any union of string, number or symbol types.
$NewTypetypeThe type each property receives. Usually written in terms of Type[Property].

Return value

A new object typeAn object type with one property per member of the iterated union. Property is bound to each key in turn and may be used on the right-hand side, typically as Type[Property].

Errors

A mapped type may not declare extra members alongside the mapping; adding one reports "A mapped type may not declare properties or methods".

Changelog

  • PHP 2.1Introduced. Partial, Readonly, Record and Pick were added to lib.d.ts on top of it.
  • PHP 2.8The + and - modifier prefixes let a mapping add or remove readonly and ?, which is how Required is written.
  • PHP 2.9Mapped types now preserve number- and symbol-named properties instead of dropping them.
  • PHP 4.1The as clause allows remapping keys, including dropping them by mapping to never.

A mapped type is a for loop over keys. { [P in keyof T]: X } visits every key of T, binds it to P, and gives the resulting property the type X — usually written in terms of T[P] so each property keeps a relationship to its original.

This is the machinery under the standard library. Partial<T> is { [P in keyof T]?: T[P] }. Readonly<T> adds readonly. Record<K, V> maps over an arbitrary union instead of a keyof. None of them are compiler magic; they are four lines each.

When to reach for it

When two types must stay in lockstep. A form-state type where every field becomes a validation message, a set of getters, an object of observables — derive them rather than maintaining a hand-written twin that will drift the first time a field is added.

What trips people up

The source does not have to be keyof anything. [K in "a" | "b"] is perfectly legal and is exactly how Record works.

Also: homomorphic mappings — the [P in keyof T] form specifically — pass modifiers, arrays and tuples straight through, so Partial<string[]> is still an array. Change the source to [P in keyof T as P] and you quietly lose that.

Example

Example · typescript
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyRecord<K extends keyof any, V> = { [P in K]: V };

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

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

type FrozenUser = MyReadonly<User>;
// { readonly id: number; readonly name: string; readonly email: string }

type Flags = MyRecord<"dev" | "prod", boolean>;
// { dev: boolean; prod: boolean }

When to use it

  • Reimplement Partial or Readonly so a shared type stays in one place
  • Turn a form's field interface into a matching errors interface
  • Build a Record-style lookup from a union of allowed status strings

Frequently asked questions

How is Partial<T> actually defined?

As a mapped type: type Partial<T> = { [P in keyof T]?: T[P] }. Readonly adds the readonly modifier instead of ?, and Required uses -? to remove optionality. None of them are built into the compiler.

Can I add an extra property to a mapped type?

No - a mapped type may only contain the mapping, and anything else reports "A mapped type may not declare properties or methods". Intersect it with a separate object type instead: { [K in keyof T]: T[K] } & { extra: string }.

Why did my mapped type turn an array into a plain object?

Because it was not homomorphic. Only the exact [P in keyof T] form preserves arrays, tuples and existing modifiers; introducing an as clause or mapping over a separately computed union drops that special handling.

More examples

Changing the value type, not just the modifiers

OptionsFlags discards the original value types entirely and replaces each with boolean, which is the pattern for feature-flag and permission maps. Errors and Nullable keep the key set locked to the source interface, so adding a field to LoginForm immediately shows up in both derived types with no edit.

Example · typescript
type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
};

type FeatureOptions = OptionsFlags<Features>;
// { darkMode: boolean; newUserProfile: boolean }

// keep the keys, wrap the values
type Errors<T> = { [K in keyof T]?: string };
type Nullable<T> = { [K in keyof T]: T[K] | null };

interface LoginForm { email: string; password: string }

type LoginErrors = Errors<LoginForm>;   // { email?: string; password?: string }
type LoginDraft = Nullable<LoginForm>;  // { email: string | null; password: string | null }

Mapping over a union, and combining with a conditional

StatusHandlers shows that any union of keys works as the source - this is exactly how Record is defined, and it guarantees every status has a handler. ExtractPII puts a conditional type on the right-hand side so each property is inspected independently, turning a schema description into a boolean map the compiler derives for you.

Example · typescript
// the source union need not come from keyof
type Statuses = "idle" | "loading" | "error";
type StatusHandlers = { [S in Statuses]: () => void };
// { idle: () => void; loading: () => void; error: () => void }

// the value type can branch per property
type ExtractPII<Type> = {
  [Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};

type DBFields = {
  id: { format: "incrementing" };
  name: { type: string; pii: true };
};

type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
// { id: false; name: true }

Discussion

  • Be the first to comment on this lesson.