Chapter 28: TypeScript Mapped Types
Mapped Types in TypeScript like we’re sitting together at a table in Hyderabad, whiteboard in front of us, typing slowly in the playground, and really understanding what happens under the hood.
Mapped types are one of the most powerful tools for creating new types by transforming (or copying with changes) the properties of an existing type. They let you iterate over keys of a type and decide what happens to each property — make them optional, readonly, change their value type, rename keys, filter some out, add prefixes, turn them into functions, etc.
They appeared early (TS 2.1) but became much more expressive with the as clause in TS 4.1 — that’s when they really exploded in popularity.
1. The absolute basic syntax (the loop over keys)
|
0 1 2 3 4 5 6 7 8 |
type OptionsFlags<Type> = { [Property in keyof Type]: boolean; }; |
- keyof Type → gives you a union of all property names (as string/number/symbol literals)
- Property in … → like a for-of loop over that union
- The result is a new object type where each key from the original gets a new value type (boolean here)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
type Features = { darkMode: () => void; newUserProfile: () => void; notificationsEnabled: boolean; }; type FeatureFlags = OptionsFlags<Features>; // { // darkMode: boolean; // newUserProfile: boolean; // notificationsEnabled: boolean; // } |
This is exactly how the built-in Partial<T>, Required<T>, Readonly<T> are implemented.
2. The most common built-in mapped types (you use them every day)
| Utility | What it does | Internal mapped type equivalent (simplified) |
|---|---|---|
| Partial<T> | All properties optional | { [P in keyof T]?: T[P] } |
| Required<T> | All properties required | { [P in keyof T]-?: T[P] } |
| Readonly<T> | All properties readonly | { readonly [P in keyof T]: T[P] } |
| Pick<T,K> | Keep only some keys | { [P in K]: T[P] } |
| Omit<T,K> | Remove some keys | { [P in keyof T as P extends K ? never : P]: T[P] } (with as) |
| Record<K,T> | Create object with specific keys → same type | { [P in K]: T } |
3. Modifiers: +? -? +readonly -readonly
These are very important — they add / remove optional / readonly flags.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
type User = { readonly id: number; name?: string; email: string; }; type MutableUser = { -readonly [P in keyof User]: User[P]; // removes readonly from id }; type RequiredUser = { [P in keyof User]-?: User[P]; // removes ? from name }; |
4. Key remapping with as clause (TS 4.1+) — the game changer
This is where mapped types become really creative.
You can change the key name during the mapping.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
type Getters<T> = { [Property in keyof T as `get${Capitalize<string & Property>}`]: () => T[Property]; }; interface Person { name: string; age: number; location?: string; } type LazyPerson = Getters<Person>; // { // getName: () => string; // getAge: () => number; // getLocation?: () => string | undefined; // } |
Very common real patterns using as in 2026
A. Prefix / suffix keys
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
type WithInternal<T> = { [K in keyof T as `internal_${string & K}`]: T[K]; }; type Settings = { port: number; host: string }; type InternalSettings = WithInternal<Settings>; // { internal_port: number; internal_host: string } |
B. Filter out keys (return never to exclude)
|
0 1 2 3 4 5 6 7 8 9 10 |
type WithoutId<T> = { [K in keyof T as K extends "id" ? never : K]: T[K]; }; type UserWithoutId = WithoutId<User>; // id is gone |
C. Event handler map from union
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type Event = { kind: "click"; x: number } | { kind: "hover"; y: number }; type EventHandlers = { [E in Event as E["kind"]]: (event: E) => void; }; // { // click: (event: { kind: "click"; x: number }) => void; // hover: (event: { kind: "hover"; y: number }) => void; // } |
D. String literal transformation (with template literals)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type Actions = "fetch" | "save" | "delete"; type ActionCreators = { [A in Actions as `create${Capitalize<A>}`]: () => void; }; // { // createFetch: () => void; // createSave: () => void; // createDelete: () => void; // } |
5. Mapped types + generics + conditionals (very powerful combo)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
type StringOnly<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K]; }; interface Mixed { id: number; title: string; description: string; active: boolean; } type OnlyStrings = StringOnly<Mixed>; // { // title: string; // description: string; // } |
6. Quick cheat-sheet table (keep this handy)
| Goal | Syntax pattern | Example output keys / notes |
|---|---|---|
| Make all optional | [P in keyof T]?: T[P] | Partial<T> |
| Make all required | [P in keyof T]-?: T[P] | Required<T> |
| Make all readonly | readonly [P in keyof T]: T[P] | Readonly<T> |
| Remove readonly | -readonly [P in keyof T]: T[P] | Mutable version |
| Rename / prefix keys | [K in keyof T as prefix_${K}]: T[K] | Needs string & K sometimes |
| Filter keys (exclude some) | [K in keyof T as K extends “bad” ? never : K] | Omit-like behavior |
| Keep only string values | [K in keyof T as T[K] extends string ? K : never] | Very useful for form fields, keys, etc. |
| Turn properties into functions | [K in keyof T as on${Capitalize<K>}]: (v: T[K]) => void | Event emitters, setters |
7. Mini homework — try these in the playground today
- Create your own Mutable<T> that removes all readonly
- Make Prefixed<T, Prefix extends string> that adds a prefix to every key
- Create StringPropsOnly<T> that keeps only properties whose value is string
- Build HandlersFromEvents<T> where T is a union of event objects with kind: string literal
Which pattern looks most useful for your current code?
Want to go deeper into:
- Combining mapped types with recursive conditionals
- Real form / validation / API payload transformations
- How libraries like Zod / tRPC use mapped types internally
- Common performance / instantiation depth issues
Just say — we’ll zoom right into that next! 😄
