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 | undefinedmessage), 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
UserandPartial<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
- Write the
Copy<T>identity mapped type and confirm it reproduces a type unchanged. - Write
Nullable<T>and apply it to aUser. Confirm every property becomesT[K] | null. - Write
Partial<T>yourself as a mapped type and confirm it matches the built-in. - Write
Readonly<T>yourself and confirm properties cannot be reassigned. - Write
Required<T>with-?to strip optionality from a type with optional fields. - Write a
Stringify<T>that turns every property into astring. - (Stretch) Write a
Getters<T>with key remapping and confirmnamebecomesgetName. Then a filter that keeps only string-valued properties.
Official documentation
- TypeScript — Mapped Types — The full feature, including modifiers.
- TypeScript — Key Remapping via
as— Renaming and filtering keys. - TypeScript — Utility Types source — Which are mapped types you can now read.
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