TypeScript Mapped Types: Transform Object Shapes
TypeScript mapped types let you derive new types from existing ones by walking over each property and applying a transformation rule. Instead of hand-writing separate interfaces for partial updates, readonly configs, or API response shapes, you define the source type once and let the mapped type generate the variants. This approach catches drift. When the source changes, every derived type updates automatically.
Introduction
Mapped types solve a recurring problem in TypeScript codebases: keeping related types in sync when a source type changes. Without them, you write a separate interface for each variant — partial updates, readonly snapshots, API DTOs — and update each one by hand when the source gains or loses a field. A mapped type applies one rule to every property, so the compiler propagates changes for you.
What are mapped types?
A mapped type creates a new type by transforming each property of an existing type. The basic syntax looks like this:
type Mapped<T> = {
[K in keyof T]: T[K];
};
Let’s break this down:
K in keyof Titerates over each key in typeTT[K]is the corresponding value type for each key
This example creates an exact copy of T. But mapped types become powerful when you add transformations.
Basic mapping transformations
Making all properties optional
type User = {
id: number;
name: string;
email: string;
};
type PartialUser = {
[K in keyof User]?: User[K];
};
// Result:
// type PartialUser = {
// id?: number | undefined;
// name?: string | undefined;
// email?: string | undefined;
// }
Adding the ? modifier on each key makes every property optional. This pattern is particularly useful for update operations and form drafts where callers supply only the fields they intend to change. The compiler enforces that any object assigned to this type can omit properties safely, and it flags attempts to read optional properties without a guard.
Making all properties readonly
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// Result:
// type ReadonlyUser = {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
The readonly modifier prevents reassignment after construction. Configuration objects and shared constants benefit from this enforcement because the compiler catches accidental writes. When you initialize a readonly object, every property must be set at creation time, which makes the initialization contract explicit and predictable.
Making all properties nullable
type NullableUser = {
[K in keyof User]: User[K] | null;
};
// Result:
// type NullableUser = {
// id: number | null;
// name: string | null;
// email: string | null;
// }
Adding | null on the value side lets every property hold a null value, which is common when deserializing API payloads where fields can legitimately be absent. TypeScript ships these transformations as built-in utility types: Partial<T>, Readonly<T>, Required<T>, and Pick<T, K>, so you rarely need to write the mapped type yourself for these cases.
Key remapping
TypeScript 4.1+ lets you remap keys using the as clause:
type User = {
firstName: string;
lastName: string;
age: number;
};
// Add "Model" suffix to each key
type UserModels = {
[K in keyof User as `${K}Model`]: User[K];
};
// Result:
// type UserModels = {
// firstNameModel: string;
// lastNameModel: string;
// ageModel: number;
// }
Key remapping shines when you generate naming conventions from existing types — model wrappers, API DTOs, or prefixed property sets. The as clause runs on every key, so the transformation is uniform across the whole type.
Filtering keys with conditional types
Combine mapped types with conditional types to filter properties:
type Props = {
name: string;
age: number;
visible: boolean;
disabled: boolean;
};
// Only keep boolean properties
type BooleanProps<T> = {
[K in keyof T as T[K] extends boolean ? K : never]: T[K];
};
type PropsFilter = BooleanProps<Props>;
// Result: type PropsFilter = { visible: boolean; disabled: boolean; }
When T[K] extends boolean ? K : never evaluates to never for a key, TypeScript drops that property from the result. This filtering pattern is how Pick and Omit work internally, and it generalizes to any discriminator you can express as a conditional type.
Practical examples
Creating a form type from a schema
type DatabaseUser = {
id: number;
username: string;
email: string;
createdAt: Date;
updatedAt: Date;
passwordHash: string;
};
// Create a form type (exclude internal/db fields)
type UserForm = {
[K in keyof DatabaseUser as
Exclude<K, "id" | "createdAt" | "updatedAt" | "passwordHash">]: DatabaseUser[K]
};
// Result:
// type UserForm = {
// username: string;
// email: string;
// }
This pattern appears often in real code: you have a database entity with internal fields like id, timestamps, and hashed secrets, and you need a clean public shape for API consumers or form inputs. Using Exclude in the as clause keeps the filter list readable even as the entity grows more fields over time.
Getters and setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
};
type PropertyAccessors<T> = Getters<T> & Setters<T>;
type UserAccessors = PropertyAccessors<{ name: string; age: number }>;
// Result includes getName(), setName(), getAge(), setAge()
Key remapping combined with template literal types makes short work of accessor generation. Every property gets a matching getter and setter signature, and the compiler guarantees the naming is consistent. This trick also works for event emitters, Redux action creators, and any factory where you want a predictable 1:1 mapping between input keys and output names.
Converting snake case to camel case
type SnakeToCamel<S extends string> =
S extends `${infer T}_${infer U}`
? `${T}${Capitalize<SnakeToCamel<U>>}`
: S;
type SnakeToCamelObject<T> = {
[K in keyof T as SnakeToCamel<K & string>]: T[K];
};
type ApiResponse = {
user_id: number;
first_name: string;
last_name: string;
created_at: string;
};
type CamelResponse = SnakeToCamelObject<ApiResponse>;
// Result:
// type CamelResponse = {
// userId: number;
// firstName: string;
// lastName: string;
// createdAt: string;
// }
Snake-to-camel conversion is a common real-world need when integrating with databases or REST APIs that use snake_case conventions. The recursive SnakeToCamel template literal type processes each underscore segment individually, so it handles deeply nested field names correctly.
Modifiers: readonly and ?
You can add or remove modifiers using + and - prefixes:
type Writable<T> = {
-readonly [K in keyof T]: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
This is how TypeScript’s built-in utility types work under the hood.
Built-in mapped types
TypeScript includes several mapped types in its standard library:
| Utility Type | Description |
|---|---|
Partial<T> | Makes all properties optional |
Required<T> | Makes all properties required |
Readonly<T> | Makes all properties readonly |
Pick<T, K> | Selects specific properties |
Omit<T, K> | Excludes specific properties |
Record<K, T> | Creates type with keys K and values T |
These are all implemented using mapped types.
Controlling shape changes
Mapped types are most useful when you want to transform a known shape in a predictable way. A common example is taking an interface and making every field optional for a draft form, or making every field readonly for a configuration object that should not change after creation. The point is not to invent a new structure from scratch. The point is to describe a rule that TypeScript can apply to every property at once.
That way of thinking helps when your project grows. Instead of hand-writing many related interfaces, you can define one source type and derive the rest. This keeps the relationship between versions obvious. If the source type changes, the derived types change with it, so you spend less time updating duplicate declarations and more time checking whether the new shape still makes sense for the feature you are building.
When mapped types get hard to read
Mapped types can become hard to scan when several modifiers stack together. If the result starts to feel cryptic, break the logic into named helper types. A short alias with a clear name is easier to maintain than a single compact expression that only the original author can parse quickly. Readability matters more than saving a few lines.
It also helps to test the type with a small example object. When you can point at a concrete value and see which properties are optional, readonly, or remapped, the type stops feeling abstract. This is especially useful in shared code, where teammates need to understand the contract without reading the full type machinery. Clear names and small examples make review comments much simpler.
A practical rule of thumb
Use mapped types when the transformation applies to every key in a predictable way. Reach for a plain interface or union when the shape needs hand-tuned exceptions. That distinction keeps the type system honest. If you find yourself adding lots of special cases, the mapped type may be doing too much work for the design.
The best mapped types usually read like a description of intent rather than a puzzle. A reader should be able to answer three questions quickly: what the source shape is, what rule is being applied, and what the new version is for. When those answers are easy to find, the type is doing its job well.
Mapping in context
Mapped types are easiest to understand when they stay close to the feature that uses them. A type that models a form draft, a patch payload, or a readonly settings object gives the reader immediate context. That context keeps the abstraction from feeling like a trick and helps the type serve the code instead of distracting from it.
When in doubt, describe the business reason for the transformation first, then write the type that matches it. That order keeps the code honest. It also makes it easier to decide whether the mapped type should stay local to one module or move into a shared helper for repeated use.
Summary
Mapped types let you transform types by iterating over their keys. Key concepts:
- Use
[K in keyof T]to iterate over all keys - Use
asto remap keys (TypeScript 4.1+) - Use conditional types with
neverto filter keys - Use
+/-prefixes to add or remove modifiers
Mapped types are the foundation for many TypeScript utility types. Once you master them, you’ll be able to create sophisticated type transformations that make your code more type-safe.
Next steps
The Conditional Types tutorial builds on mapped types to show how TypeScript lets you branch on type relationships. Conditional types are the basis for utility types like Exclude and Extract, and they pair naturally with the key filtering patterns you saw here.