C.W.K.
Stream
Lesson 04 of 06 · published

Mapped Types: Looping Over Keys

~11 min · type-manipulation, mapped-types, type-transformation

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Mapped types are type-level loops. Iterate over keys, produce a new shape."

The mapped-type syntax

{ [K in keyof T]: NewType } creates a new object type by iterating over T's keys. For each key K, the property's value type is whatever expression you write after the colon — usually involving K, T[K], or both.

This is how Partial, Required, Readonly, and many utility types are built. Looking at one:

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

Loop over T's keys, mark each optional with ?, keep the original value type. Three changes from the identity mapped type, none of them complicated.

Modifiers: + and -

Mapped types can add or remove modifiers explicitly. +? adds optional, -? removes optional. +readonly adds readonly, -readonly removes it. Required<T> uses -? to strip optionality.

The plus signs are usually omitted (they're the default direction). The minus signs are what you write when you want to remove a modifier the keys already have.

Key remapping with as

TypeScript 4.1 added as to mapped types: { [K in keyof T as NewKey]: ... }. The NewKey can be a string-manipulating type expression (often a template literal) that transforms the key on its way through. This lets you build types like 'add a get prefix to every key.'

Mapped types are the foundation of most type-level transformations. Once you can write one, you can build any of the utility types from scratch — and you'll find yourself reaching for them when none of the built-ins quite fits.

Code

Mapped types — every utility built from these patterns·typescript
// Identity — useful for understanding the syntax.
type Identity<T> = { [K in keyof T]: T[K] };

// Partial — add `?` to every key.
type MyPartial<T> = { [K in keyof T]?: T[K] };

// Required — strip `?` from every key.
type MyRequired<T> = { [K in keyof T]-?: T[K] };

// Readonly — add `readonly` to every key.
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

// Make every property null-or-original.
type Nullable<T> = { [K in keyof T]: T[K] | null };

// Key remapping with `as` — add 'get' prefix.
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

External links

Exercise

Write a Stringify<T> mapped type that converts every property of T to a string type. Stringify<{ a: number; b: boolean }> should be { a: string; b: string }. Then write a Promiseify<T> that wraps every value in Promise<T[K]>. Confirm both behave as you expect.
Hint
type Stringify<T> = { [K in keyof T]: string }. type Promiseify<T> = { [K in keyof T]: Promise<T[K]> }. The pattern stays the same; only the right side of the colon changes.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.