TypeScript Mapped Types
A mapped type creates a new type by transforming the properties of an existing type. Instead of writing a new type from scratch, a mapped type iterates over the keys of an existing type and applies a transformation — making all properties optional, readonly, or changing their value types. Mapped types are the engine behind many of TypeScript's built-in utility types.
The Concept
Original type: Mapped type (all properties optional): +----------+----------+ +----------+-----------+ | name | string | | name? | string | | age | number | | age? | number | | email | string | | email? | string | +----------+----------+ +----------+-----------+ Every property required Every property optional
Basic Mapped Type Syntax
{ [K in keyof T]: TransformedType }
| | |
| | +-- New type for each property
| +----------------- Iterate over all keys of T
+------------------------ K = current key name
// Make every property optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
type User = {
name: string;
age: number;
email: string;
};
type PartialUser = Optional<User>;
// { name?: string; age?: number; email?: string }
let u: PartialUser = { name: "Rohini" }; // Valid — other fields optional
Making All Properties Readonly
type Immutable<T> = {
readonly [K in keyof T]: T[K];
};
type Config = {
host: string;
port: number;
debug: boolean;
};
type FrozenConfig = Immutable<Config>;
// { readonly host: string; readonly port: number; readonly debug: boolean }
let cfg: FrozenConfig = { host: "localhost", port: 3000, debug: false };
cfg.host = "example.com"; // Error: Cannot assign to 'host' — readonly
Making All Properties Required
// Remove the optional marker with -?
type Required<T> = {
[K in keyof T]-?: T[K];
};
type DraftForm = {
name?: string;
email?: string;
phone?: string;
};
type SubmittedForm = Required<DraftForm>;
// { name: string; email: string; phone: string } ← all required now
Removing Readonly
// Remove readonly modifier with -readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type FrozenPoint = {
readonly x: number;
readonly y: number;
};
type MutablePoint = Mutable<FrozenPoint>;
// { x: number; y: number } ← writable now
Mapping to a Different Type
// Convert all property values to boolean flags
type Flags<T> = {
[K in keyof T]: boolean;
};
type Student = {
name: string;
age: number;
grade: string;
};
type StudentFlags = Flags<Student>;
// { name: boolean; age: boolean; grade: boolean }
// Useful for tracking which fields have been edited
let editedFields: StudentFlags = {
name: true,
age: false,
grade: true
};
Key Remapping (as Clause)
TypeScript 4.1 introduced the as clause inside mapped types for renaming keys during mapping.
// Prefix every property name with "get"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Person = {
name: string;
age: number;
};
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }
Filtering Keys with Key Remapping
// Keep only string-value properties
type StringOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type Product = {
id: number;
name: string;
price: number;
category: string;
active: boolean;
};
type StringFields = StringOnly<Product>;
// { name: string; category: string }
Nested Mapped Types
// Make all nested properties optional too (deep partial)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type CourseConfig = {
title: string;
settings: {
maxStudents: number;
allowDownload: boolean;
duration: {
hours: number;
minutes: number;
};
};
};
type DraftCourse = DeepPartial<CourseConfig>;
// All properties, including nested ones, become optional
let draft: DraftCourse = {
title: "TypeScript Advanced",
settings: {
maxStudents: 30
// allowDownload and duration can be omitted
}
};
Mapped Type with Value Transformation
// Wrap every property in an array
type ArrayValues<T> = {
[K in keyof T]: T[K][];
};
type Score = {
math: number;
science: number;
english: number;
};
type ScoreHistory = ArrayValues<Score>;
// { math: number[]; science: number[]; english: number[] }
let history: ScoreHistory = {
math: [85, 90, 78],
science: [88, 92],
english: [75, 80, 85, 70]
};
Built-In Utility Types That Use Mapping
| Utility Type | What It Does | Equivalent Mapped Type |
|---|---|---|
Partial<T> | All properties optional | [K in keyof T]?: T[K] |
Required<T> | All properties required | [K in keyof T]-?: T[K] |
Readonly<T> | All properties readonly | readonly [K in keyof T]: T[K] |
Record<K, V> | Object with keys K and values V | [P in K]: V |
Practical Example
// Form state management with mapped types
type FormState<T> = {
values: T;
touched: { [K in keyof T]: boolean };
errors: { [K in keyof T]: string | null };
dirty: { [K in keyof T]: boolean };
};
type LoginForm = {
email: string;
password: string;
};
let loginState: FormState<LoginForm> = {
values: {
email: "",
password: ""
},
touched: {
email: false,
password: false
},
errors: {
email: null,
password: null
},
dirty: {
email: false,
password: false
}
};
// User types in the email field
loginState.values.email = "user@estudy247.com";
loginState.touched.email = true;
loginState.dirty.email = true;
// Validate
if (!loginState.values.email.includes("@")) {
loginState.errors.email = "Invalid email address";
}
console.log(loginState.values.email); // user@estudy247.com
console.log(loginState.errors.email); // null
