TypeScript Mapped Types
Mapped types are one of TypeScript’s most powerful type-system features. They let you transform existing types into new ones by iterating over their keys. If you’ve ever wanted to make all properties optional, readonly, or nullable, mapped types are the tool for the job.
In this tutorial, you’ll learn how mapped types work, how to use built-in mapped types, and how to create your own.
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 T— iterates over each key in typeTT[K]— 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;
// }
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;
// }
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;
// }
TypeScript provides these transformations as built-in utility types: Partial<T>, Readonly<T>, Required<T>, and Pick<T, K>.
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;
// }
This is incredibly useful for creating derivative types.
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; }
The never type removes keys from the resulting type entirely.
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;
// }
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()
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;
// }
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.
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.
What’s Next?
In the next tutorial, we’ll explore Conditional Types, which let you create types that depend on other types—essential for advanced TypeScript patterns.