When you need to transform every property of a type—whether that's wrapping each value in a function, toggling optional flags, or renaming keys—you reach for mapped types. They're one of TypeScript's most powerful features, turning repetitive type definitions into a single, maintainable template. In this article, we'll build from syntax fundamentals through real production patterns.
Understanding mapped types: the syntax
A mapped type iterates over the keys of an existing type and produces a new type by transforming each property. The core syntax follows this pattern:
{ [K in keyof T]: ... }Let me break this down. K in keyof T loops through every key in type T. For each key K, we define a new property with the same or transformed name, and assign it a new type.
Here's a simple example:
type User = {
id: number;
name: string;
email: string;
};
type UserAsStrings = {
[K in keyof User]: string;
};
// Result:
// {
// id: string;
// name: string;
// email: string;
// }Every property becomes a string, regardless of its original type. That's not particularly useful on its own, but it demonstrates the mechanics. The real power emerges when we reference the original type's properties using T[K].
type Readonly_copy = {
readonly [K in keyof T]: T[K];
};
type UserReadonly = Readonly_copy;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }Now we're keeping each property's original type but adding readonly. This is exactly what TypeScript's built-in Readonly utility type does.
Modifiers: readonly and optional transformations
Mapped types support modifiers: readonly and the optional marker ?. You can add or remove them with + and - prefixes.
Imagine you're building a form validation system. Your API returns immutable data, but your form needs mutable versions where every field might be optional during editing:
type ApiResponse = {
readonly id: number;
readonly name: string;
readonly email: string;
};
type FormState = {
-readonly [K in keyof T]?: T[K];
};
type EditableForm = FormState;
// {
// id?: number;
// name?: string;
// email?: string;
// }The -readonly removes the readonly modifier. The ? makes each field optional. You can also use +readonly (explicit addition, though readonly alone works) and -? to remove optional marks.
Another practical example: a configuration object where you want to guarantee every field is set (not optional), but you're transforming from a partial input type:
type PartialConfig = {
timeout?: number;
retries?: number;
debug?: boolean;
};
type RequiredConfig = {
[K in keyof T]-?: T[K];
};
type StrictConfig = RequiredConfig;
// {
// timeout: number;
// retries: number;
// debug: boolean;
// }The -? removes the optional marker entirely. This is powerful when you're normalizing partial inputs into guaranteed internal state.
Key remapping: renaming properties with as
Beyond transforming values and modifiers, you can rename keys using the as clause with template literals. This unlocks patterns like generating getters, event handlers, or API method names from a single property definition.
Consider a scenario where your domain object has simple properties, but you want to auto-generate getter methods with a predictable naming convention:
type User = {
id: number;
name: string;
email: string;
};
type Getters = {
[K in keyof T as `get${Capitalize}`]: () => T[K];
};
type UserGetters = Getters;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }The as `get${Capitalize}` transforms the key. Capitalize is a built-in TypeScript utility that capitalizes the first character. The string & K constrains K to strings (since object keys can technically be symbols, but we want string keys for template literals).
You can filter out properties too. Suppose you only want getters for properties that are not numbers:
type GettersForNonNumbers = {
[K in keyof T as T[K] extends number ? never : `get${Capitalize}`]: () => T[K];
};
type UserGettersPartial = GettersForNonNumbers;
// {
// getName: () => string;
// getEmail: () => string;
// }When the condition resolves to never, that property is excluded from the resulting type. This is conditional mapping—incredibly useful for type-safe API client generation or event handler builders.
Real-world use: form state and validation
Let's build a practical form state management system using mapped types. You define your domain model once, and mapped types derive the form state shape, error shape, and touched state:
type UserFormData = {
firstName: string;
lastName: string;
email: string;
age: number;
};
// Errors for each field: string or undefined
type FormErrors = {
[K in keyof T]?: string;
};
// Track which fields have been touched
type FormTouched = {
[K in keyof T]: boolean;
};
// Form state combines values, errors, and touched flags
type FormState = {
values: T;
errors: FormErrors;
touched: FormTouched;
};
type UserFormState = FormState;
// {
// values: UserFormData;
// errors: { firstName?: string; lastName?: string; email?: string; age?: string; };
// touched: { firstName: boolean; lastName: boolean; email: boolean; age: boolean; };
// }Define your form once, and all related types follow automatically. When you add a new field to UserFormData, the error and touched types scale with zero additional effort.
Real-world use: API client generation
Many teams generate API clients from backend contracts. Mapped types help ensure consistency without manual repetition:
type UserDTO = {
id: number;
name: string;
createdAt: string;
};
// Generate create endpoint (omit id and createdAt)
type Omit = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type CreateUserPayload = Omit;
// { name: string; }
// Generate update endpoint (make everything optional)
type UpdateUserPayload = {
[K in keyof CreateUserPayload]?: CreateUserPayload[K];
};
// { name?: string; }
// Generate API methods
type ApiMethods = {
[K in keyof T as `fetch${Capitalize}`]: (id: number) => Promise;
};
// Hypothetical resources object
type Resources = {
user: UserDTO;
post: { id: number; title: string };
};
type ClientMethods = ApiMethods;
// {
// fetchUser: (id: number) => Promise;
// fetchPost: (id: number) => Promise<{ id: number; title: string; }>;
// }This scales to hundreds of endpoints with consistency guaranteed by the type system.
Real-world use: validation schema builders
If you're using a validation library like Zod or io-ts, mapped types help sync your runtime validators with your TypeScript types:
type User = {
name: string;
email: string;
age: number;
};
// Placeholder for a validation schema builder
type Validator = (value: unknown) => T;
type Validators = {
[K in keyof T]: Validator;
};
const userValidators: Validators = {
name: (val) => {
if (typeof val !== 'string') throw new Error('name must be string');
return val;
},
email: (val) => {
if (!val.includes('@')) throw new Error('invalid email');
return val;
},
age: (val) => {
if (typeof val !== 'number') throw new Error('age must be number');
return val;
},
};
// Each validator's input and output is type-safe relative to User
This ensures that your runtime validators exactly match your type definitions—a crucial guard against subtle bugs.
Common mistakes
Forgetting keyof: Without keyof T, you can't iterate. [K in T] will fail—you must use [K in keyof T].
Mixing up T[K] and K: T[K] retrieves the type of property K in type T. Just using K gives you the key name (usually a string literal type). If you need the property type, use T[K].
Not constraining for template literals: Template literals in mapped types only work with string-like keys. Use K extends string or string & K to filter out symbols and guarantee string keys.
Forgetting that never excludes: In conditional mapped types, returning never as the key removes that property entirely. This is intentional but easy to miss if you're expecting an empty property.
Mutating the original type: Mapped types create new types; they don't modify the original. If you want to transform a type in-place, you're thinking about it wrong—define the transformation as a mapped type and assign it to a new name.
Key takeaways
- Syntax: Use
{ [K in keyof T]: T[K] }to loop through all properties and transform them. - Modifiers: Control
readonlyand optional marks with+and-prefixes to reshape constraints. - Remapping: Use
aswith template literals to rename or filter keys, enabling powerful code generation patterns. - DRY principle: Define your core type once; let mapped types derive related shapes (errors, getters, validators, API methods).
- Production patterns: Form state management, API client generation, and validation builders all benefit from mapped types, reducing repetition and keeping types in sync.
- Composability: Combine mapped types with conditional types and utility types for expressive, maintainable type definitions.
Mapped types bridge the gap between static type definitions and the dynamic transformations your application needs. Mastering them unlocks cleaner, more scalable TypeScript codebases—especially as you work on larger platforms where consistency across dozens of types matters.






Comments
Loading comments…