TypeScript Mapped Types Deep Dive
A practical tour of mapped types in TypeScript, including key remapping, modifiers, and patterns that build powerful generic utilities.
What you'll learn
- ✓How mapped types transform object shapes
- ✓Using keyof, in, and as for key remapping
- ✓Applying readonly and optional modifiers
- ✓Composing mapped types with conditionals
- ✓Building reusable utility types
Prerequisites
- •Comfortable with JS
- •Basic TypeScript generics
What and Why
A mapped type walks over the keys of a type and produces a new type by transforming each property. They are the engine behind Partial, Required, Readonly, and Pick. Learning them is the difference between copy-pasting utility types and writing your own that fit your domain.
The reason mapped types are powerful is simple: instead of repeating the same change across many interfaces, you express the change once and apply it everywhere. That makes large codebases easier to evolve.
Mental Model
A mapped type looks like an object literal where the key is computed using in keyof. Think of it as a for loop at the type level.
for each K in keyof T:
new property [K] = transform(T[K])
{ [K in keyof T]: NewType<T[K]> } You can change three things at each step: the key, the value, and the modifiers (readonly, ?).
Hands-on Example
Start with a User type and build several mapped utilities on top of it.
interface User {
id: number;
name: string;
email: string;
isAdmin: boolean;
}
A basic Partial lookalike:
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type PartialUser = MyPartial<User>;
// { id?: number; name?: string; ... }
Stripping readonly with the - modifier:
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Renaming keys using as. Here we prefix every key with db_:
type DbColumns<T> = {
[K in keyof T as `db_${string & K}`]: T[K];
};
type UserColumns = DbColumns<User>;
// { db_id: number; db_name: string; db_email: string; db_isAdmin: boolean }
Filtering keys by value type. We keep only properties whose value extends string:
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringKeys<User>;
// { name: string; email: string }
keyof User: id | name | email | isAdmin
for each K:
if T[K] extends string -> keep K
else -> never (drop)
result: name | email Combining with conditional types, you can build a deep Readonly:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
This walks every nested object and freezes it at the type level.
Common Pitfalls
Confusing keyof T with the values of T. keyof User gives "id" | "name" | "email" | "isAdmin", not the property values. When you index with T[K], you get the value type.
Using template literal keys with non-string keys. If K can be number or symbol, the template `${K}` will not work. Constrain with string & K or filter keys first.
Forgetting -? versus ?. The minus removes optionality; without it, you only add it. The same applies to readonly versus -readonly.
Applying mapped types to union types unexpectedly. A mapped type distributes over a naked type parameter if it appears in a conditional. To prevent distribution, wrap the parameter in a tuple.
Building mapped types on Record<string, unknown> and wondering why every key is allowed. Use a constrained generic like T extends object to keep inference sharp.
Best Practices
Name your utilities after intent, not mechanics. Nullable<T> reads better than MakeNullish<T>. The next person reading the code does not care how it works, only what it produces.
Lean on TypeScript’s existing utility types when they fit. Partial, Required, Readonly, Pick, Omit, and Record cover most everyday cases. Only build your own when you find yourself repeating a transformation.
Test types with expectTypeOf or tsd. Type tests are cheap and catch regressions when you refactor a mapped type that hundreds of call sites depend on.
Keep transformations small. If you need three modifiers and a rename, write three smaller mapped types and compose them. Easier to read, easier to debug.
Use infer within conditional types to extract pieces, then feed them into a mapped type. This combination unlocks expressing things like “make all functions on this type async.”
Wrap-up
Mapped types let you express transformations on whole shapes in a single line. With keyof, as, and the readonly/? modifiers, you can rename, filter, and modify properties to fit your domain. Use the built-in utilities first, then graduate to custom ones when patterns repeat. Master these, and your types start to feel like a small programming language working for you instead of fighting you.
Related articles
- TypeScript TypeScript infer Keyword Explained
Master the infer keyword in TypeScript conditional types. Learn how to extract parts of complex types, build utility helpers, and write expressive generics.
- TypeScript TypeScript Recursive Types Tutorial
Build recursive types in TypeScript: deep readonly, JSON, paths, and tuple manipulation. Learn how to write recursion that the compiler can actually evaluate.
- TypeScript TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.
- TypeScript TypeScript Generics with React
A practical guide to using TypeScript generics in React components, hooks, and props for safer, more reusable building blocks.