Mapped Types: Transforming Types in TypeScript


Mapped types are TypeScript's equivalent of Array.map for type transformations. Just as map takes an array and produces a new array by applying a function to every element, a mapped type takes an object type and produces a new type by applying a transformation to every property. They're the mechanism behind Partial<T>, Required<T>, Readonly<T>, and Record<K, V> — and once you understand the pattern, you can build your own. In this post, you'll learn how to write mapped types, add and remove property modifiers, remap keys, and filter properties using conditional types.
Mapped Type Mental Model
A mapped type says: "For every key K in this union of keys, produce a
property with key K and value type ...." The [K in Keys] syntax is the
iteration — K is a type variable that takes each value in Keys one at a
time.
What Are Mapped Types
A mapped type creates a new object type by iterating over a set of keys. The most basic form uses keyof to iterate over the keys of an existing type:
type Copy<T> = {
[K in keyof T]: T[K];
};
interface User {
id: number;
name: string;
active: boolean;
}
type CopiedUser = Copy<User>;
// { id: number; name: string; active: boolean }
// Structurally identical to UserThe [K in keyof T] part iterates: K takes the value "id", then "name", then "active". For each, T[K] is the type of that property — number, string, boolean.
Basic Mapped Type Syntax
The real power comes from transforming the value type on the right-hand side:
// Make every property a string
type Stringify<T> = {
[K in keyof T]: string;
};
// Wrap every property in an array
type Arrayify<T> = {
[K in keyof T]: T[K][];
};
// Wrap every property in Promise
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
type User = { id: number; name: string };
type StringUser = Stringify<User>; // { id: string; name: string }
type ArrayUser = Arrayify<User>; // { id: number[]; name: string[] }
type AsyncUser = Promisify<User>; // { id: Promise<number>; name: Promise<string> }You can also map over a specific union of string literal types, not just keyof:
type Flags = {
[K in "read" | "write" | "delete"]: boolean;
};
// { read: boolean; write: boolean; delete: boolean }Mapping with Modifiers
Mapped types can add or remove the ? (optional) and readonly modifiers. This is how Partial<T> and Required<T> work:
// TypeScript's built-in Partial<T>
type Partial<T> = {
[K in keyof T]?: T[K];
};
// TypeScript's built-in Required<T>
type Required<T> = {
[K in keyof T]-?: T[K]; // The minus removes the optional modifier
};
// TypeScript's built-in Readonly<T>
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Remove readonly from all properties
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};Adding and Removing Modifiers
The + prefix adds a modifier (it's the default), and the - prefix removes it:
interface Config {
readonly host: string;
readonly port?: number;
readonly debug?: boolean;
}
// Remove readonly and make everything required
type WritableRequired<T> = {
-readonly [K in keyof T]-?: T[K];
};
type MutableConfig = WritableRequired<Config>;
// { host: string; port: number; debug: boolean }Mutable Is Not Built In
TypeScript doesn't ship a Mutable<T> utility type out of the box. The -readonly [K in keyof T]: T[K] pattern is the standard way to write it yourself — and it comes up often in testing code where you need to set read-only fields.
Key Remapping with as
TypeScript 4.1 added key remapping via the as clause. This lets you transform the key names themselves — not just their value types:
// Prefix all keys with "get"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
id: number;
name: string;
}
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }The as clause takes a template literal type to transform the key. Capitalize<string & K> capitalizes the first letter of each key name.
You can also remap to completely different keys:
type EventMap<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type FormEvents = EventMap<{ submit: FormData; reset: void; change: string }>;
// { onSubmit: (value: FormData) => void; onReset: (value: void) => void; onChange: (value: string) => void }Filtering Properties
Combine key remapping with conditional types to filter out properties:
// Keep only properties whose value type extends a given type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
label: string;
}
type StringProperties = PickByValue<Mixed, string>;
// { name: string; label: string }
type NumberProperties = PickByValue<Mixed, number>;
// { id: number; count: number }When the as expression evaluates to never, that key is excluded from the result — just like never drops out of union types.
never Filters Keys
In a mapped type's as clause, returning never removes that key from the
output. This is a powerful filtering mechanism, but it only works in key
remapping — not in the value position.
Real-World Patterns
Here's a real-world pattern for building a form validation schema from a data type:
type ValidationRule<T> = {
required?: boolean;
validate?: (value: T) => string | undefined;
};
type FormSchema<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
const loginSchema: FormSchema<LoginForm> = {
email: {
required: true,
validate: (v) => (v.includes("@") ? undefined : "Invalid email"),
},
password: {
required: true,
validate: (v) => (v.length >= 8 ? undefined : "Too short"),
},
rememberMe: {
required: false,
},
};TypeScript enforces that loginSchema has exactly the same keys as LoginForm, each with the right validation rule shape. Add or remove a field from LoginForm and the schema will break at compile time until you update it.
Keeping Types in Sync
Mapped types are what keep your derived types synchronized with their source.
When you add a field to LoginForm, TypeScript immediately flags every mapped
type that needs updating — a compile-time refactoring guide.
Best Practices
Use mapped types for structural transformations, not business logic. If you're transforming types based on application rules, consider whether a simpler approach (discriminated union, interface extension) would be clearer.
Compose mapped types. You can chain them: Readonly<Partial<T>> is perfectly valid and readable.
Name intermediate types. If a mapped type is complex, break it into named steps:
type Nullable<T> = { [K in keyof T]: T[K] | null };
type ReadonlyNullable<T> = Readonly<Nullable<T>>;Avoid mutating modifiers unnecessarily. Removing readonly everywhere is a smell. If you need to mutate a readonly value, consider whether your design should change rather than working around the constraint.
Anti-Pattern
Don't use mapped types when Pick, Omit, or Partial already does what you
need. Reaching for a custom mapped type before checking the standard library
is premature complexity.
What Is Next
You've seen mapped types iterate over keyof T — but what exactly does keyof produce? The next post covers the keyof and typeof operators in depth: how they work, what types they produce, and how they combine to build powerful type queries.
Key Takeaways
- Mapped types use
[K in keyof T]: ...to iterate over the keys of a type and produce a new type - The
?andreadonlymodifiers can be added or removed using+?,-?,+readonly, and-readonly - The
asclause in key remapping transforms key names and can filter keys by returningnever - Combining mapped types with conditional types enables property filtering by value type
- Standard library types
Partial,Required,Readonly, andRecordare all mapped types
Mapped types are the backbone of TypeScript's structural transformation system. With them, you can derive any variation of a type — optional, readonly, promisified, validated — without ever writing the same type shape twice.