RizTech Academy logo
RizTech Academy
The Type System in DepthLesson 2 of 635 min

Mapped types: transforming every property

A mapped type transforms every property of a type at once — making them all optional, all readonly, all nullable, or any transformation you can express. This is how Partial, Readonly, and Record are actually built, and once you can write a mapped type, half of TypeScript's "magic" utility types become something you could have written yourself. It builds directly on keyof and indexed access from the last lesson.

The idea: loop over the keys

A mapped type looks like an object type, but instead of listing properties, it loops over the keys of another type and produces a property for each:

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

Read [K in keyof T] as "for each key K in the keys of T". For each such key, this produces a property K with type T[K] (indexed access — the original property's type). So Copy<T> is a type identical to T — it copies every property unchanged. On its own that is useless, but it is the skeleton: { [K in keyof T]: ... } iterates over T's properties, and you transform the .... The in here is the mapped-type syntax (different from the in narrowing operator), and keyof T is the set it iterates.

Transforming the value type

Change what comes after the colon and you transform every property's type:

type Stringify<T> = {
  [K in keyof T]: string;      // every property becomes a string
};

type Nullable<T> = {
  [K in keyof T]: T[K] | null;   // every property becomes "its type OR null"
};

interface User { id: string; age: number; }

type StringUser = Stringify<User>;   // { id: string; age: string }
type NullableUser = Nullable<User>;  // { id: string | null; age: number | null }

Nullable<T> is genuinely useful: it takes any type and makes every property allow null, keeping the original types. This is a real transformation — one small mapped type applied to any object type. You write the rule once (T[K] | null), and it applies to every property of every type you use it on.

Modifiers: ? and readonly

Mapped types can add or remove the ? (optional) and readonly modifiers — which is exactly how the built-in utilities work:

// Partial<T> — this IS how it is defined in TypeScript's standard library:
type Partial<T> = {
  [K in keyof T]?: T[K];        // add ? to every property
};

// Readonly<T>:
type Readonly<T> = {
  readonly [K in keyof T]: T[K];   // add readonly to every property
};

type MyPartialUser = Partial<User>;   // { id?: string; age?: number }

Adding ? after [K in keyof T] makes every property optional — that is the entire definition of Partial. Adding readonly before it makes every property readonly — that is Readonly. When you learned these utility types were "not magic", this is what that meant: Partial<T> is a two-line mapped type. You could have written it.

You can also remove modifiers with a - prefix:

type Required<T> = {
  [K in keyof T]-?: T[K];       // -? REMOVES optional from every property
};

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];   // -readonly REMOVES readonly
};

-? strips optionality (that is Required<T>), and -readonly strips readonly (a Mutable<T>, useful when you have a readonly type you need to build up mutably). Adding is the default; - removes.

Key remapping with as — renaming keys

A newer, powerful feature: you can rename the keys as you map, with an as clause. This is how you'd build, say, a type of getter methods from a data type:

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

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

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

The as \get${Capitalize<...>}`renames each key —namebecomesgetName` — using a template literal type (the next-but-one lesson). This is advanced, but it shows the ceiling: mapped types can transform property names as well as types, which is how libraries generate one shape from another (a form type from a model, event handlers from events). You will not write these often, but recognising them stops a library's types from looking like sorcery.

Key remapping also lets you filter properties — mapping a key to never removes it:

type StringKeysOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
// keeps only the properties whose value is a string

(That T[K] extends string ? K : never is a conditional type, the next lesson — mapped and conditional types compose.)

Why mapped types matter

Mapped types are the "transform a whole type" tool, and they are everywhere behind the scenes:

  • Every property-modifying utility type — Partial, Required, Readonly, Pick, Record — is a mapped type. Now you can read their definitions.
  • Deriving related types — a form's "errors" type (every field becomes a string | undefined message), a "loading" type, an API's "patch" type — all mapped types over a base type.
  • Keeping types in sync — because a mapped type is derived from its input, changing the input updates every derived type automatically. Add a field to User and Partial<User>, Nullable<User>, and your custom mapped types all include it.

You will not write a mapped type every day, but you will read them constantly (in libraries and utility types), and when you need to transform every property of a type, this is the one tool that does it. Being able to write Nullable<T> or Partial<T> from scratch is the moment TypeScript's type system clicks from "mysterious" to "a language I can program in".

Check your work

What a mapped type does. Transforms every property of a type at once by looping over its keys.

The skeleton. { [K in keyof T]: ... } — for each key K in T, produce a property; transform the ....

How to transform the value type. Change what follows the colon — T[K] | null for Nullable, string for Stringify.

How Partial and Readonly are defined. Mapped types that add ? and readonly respectively to every property — they are not magic.

How to remove modifiers. -? removes optional (that is Required); -readonly removes readonly (a Mutable).

What key remapping with as does. Renames (or filters, via never) keys as you map — using template literal types for renaming.

Where mapped types appear. Every property-modifying utility type, derived related types (form errors, patches), and keeping types in sync.

Why "not magic" matters. Partial<T> is a two-line mapped type you could write yourself — the type system is programmable.

Practice

  1. Write the Copy<T> identity mapped type and confirm it reproduces a type unchanged.
  2. Write Nullable<T> and apply it to a User. Confirm every property becomes T[K] | null.
  3. Write Partial<T> yourself as a mapped type and confirm it matches the built-in.
  4. Write Readonly<T> yourself and confirm properties cannot be reassigned.
  5. Write Required<T> with -? to strip optionality from a type with optional fields.
  6. Write a Stringify<T> that turns every property into a string.
  7. (Stretch) Write a Getters<T> with key remapping and confirm name becomes getName. Then a filter that keeps only string-valued properties.

Official documentation

Next: conditional types.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship