6.3 Mapped Types
What Are Mapped Types?
Mapped types are a powerful TypeScript feature that iterates over the properties of an existing type and produces a new one. Just like the map method transforms each element of an array, a mapped type can transform each key or value type of an object type.
// Basic form
type MappedType<T> = {
[K in keyof T]: T[K]; // iterate over every key of T
};
Mapped types let you:
- Eliminate repetitive type definitions.
- Automatically generate derived types from an existing type.
- Understand how built-in utility types like
Partial,Readonly,Pick, andRecordwork internally.
Core Concepts
1. Basic Mapped Type Syntax
interface User {
id: number;
name: string;
email: string;
}
// Convert all properties to string
type Stringified<T> = {
[K in keyof T]: string;
};
type StringifiedUser = Stringified<User>;
// { id: string; name: string; email: string; }
// Convert values to getter functions
type Getters<T> = {
[K in keyof T]: () => T[K];
};
type UserGetters = Getters<User>;
// { id: () => number; name: () => string; email: () => string; }
2. +/- Modifiers
The + (add) and - (remove) prefixes control readonly and ? modifiers.
interface Config {
host: string;
port: number;
timeout?: number;
}
// +? : make all properties optional
type AllOptional<T> = {
[K in keyof T]+?: T[K];
};
// -? : remove all optional markers (same as Required)
type AllRequired<T> = {
[K in keyof T]-?: T[K];
};
// +readonly : make all properties read-only
type AllReadonly<T> = {
+readonly [K in keyof T]: T[K];
};
// -readonly : remove readonly (Mutable)
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Combined: remove readonly and optional at the same time
type WritableRequired<T> = {
-readonly [K in keyof T]-?: T[K];
};
3. as Clause — Key Remapping
TypeScript 4.1+ lets you transform key names with an as clause.
// Rename keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Filter keys by removing them with never
type NonNullableFields<T> = {
[K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
};
interface Profile {
id: number;
nickname: string | null;
bio: string | undefined;
avatarUrl: string;
}
type RequiredProfile = NonNullableFields<Profile>;
// { id: number; avatarUrl: string; }
4. Mapped Types + Conditional Types
// Apply different types based on the value type
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Extract only function properties
type FunctionProperties<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
};
// Extract only non-function properties
type NonFunctionProperties<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};
interface Service {
name: string;
version: number;
connect(): Promise<void>;
disconnect(): void;
query(sql: string): Promise<unknown[]>;
}
type ServiceConfig = NonFunctionProperties<Service>;
// { name: string; version: number; }
type ServiceMethods = FunctionProperties<Service>;
// { connect: () => Promise<void>; disconnect: () => void; query: (sql: string) => Promise<unknown[]>; }
Implementing Utility Types
// Partial<T> — make all properties optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Required<T> — remove all optional markers
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Readonly<T> — make all properties read-only
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Pick<T, K> — keep only selected keys
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit<T, K> — exclude selected keys
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
// Record<K, V> — key-value pair type
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
// Usage
interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
type PartialTodo = MyPartial<Todo>;
// { id?: number; title?: string; completed?: boolean; createdAt?: Date; }
type TodoPreview = MyPick<Todo, 'id' | 'title'>;
// { id: number; title: string; }
type TodoWithoutDates = MyOmit<Todo, 'createdAt'>;
// { id: number; title: string; completed: boolean; }
Code Examples
Example 1: Auto-Generate Getters and Setters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type GettersAndSetters<T> = Getters<T> & Setters<T>;
interface ProductData {
name: string;
price: number;
inStock: boolean;
}
// Automatically generated type:
// {
// getName: () => string;
// getPrice: () => number;
// getInStock: () => boolean;
// setName: (value: string) => void;
// setPrice: (value: number) => void;
// setInStock: (value: boolean) => void;
// }
type ProductAccessors = GettersAndSetters<ProductData>;
// Factory that creates actual accessors at runtime
function createAccessors<T extends object>(data: T): T & GettersAndSetters<T> {
const accessors: any = { ...data };
for (const key of Object.keys(data) as (keyof T)[]) {
const capitalizedKey = String(key).charAt(0).toUpperCase() + String(key).slice(1);
accessors[`get${capitalizedKey}`] = () => data[key];
accessors[`set${capitalizedKey}`] = (value: T[typeof key]) => {
(data as any)[key] = value;
};
}
return accessors;
}
Example 2: EventHandlers Type
// Auto-generate event handler types
type EventHandlers<T extends Record<string, unknown>> = {
[K in keyof T as `on${Capitalize<string & K>}`]?: (event: T[K]) => void;
};
// DOM-style event map
interface ButtonEventMap {
click: MouseEvent;
focus: FocusEvent;
blur: FocusEvent;
keydown: KeyboardEvent;
}
type ButtonProps = {
label: string;
disabled?: boolean;
} & EventHandlers<ButtonEventMap>;
// {
// label: string;
// disabled?: boolean;
// onClick?: (event: MouseEvent) => void;
// onFocus?: (event: FocusEvent) => void;
// onBlur?: (event: FocusEvent) => void;
// onKeydown?: (event: KeyboardEvent) => void;
// }
// Custom event emitter type
type EventEmitter<Events extends Record<string, unknown>> = {
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): void;
off<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): void;
emit<K extends keyof Events>(event: K, data: Events[K]): void;
};
Example 3: Implementing DeepReadonly
// Make every property readonly at all nesting levels
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
server: {
port: number;
ssl: boolean;
allowedOrigins: string[];
};
}
type ImmutableConfig = DeepReadonly<Config>;
const config: ImmutableConfig = {
database: {
host: 'localhost',
port: 5432,
credentials: {
username: 'admin',
password: 'secret',
},
},
server: {
port: 3000,
ssl: true,
allowedOrigins: ['https://example.com'],
},
};
// All of the following produce compile errors:
// config.database.host = 'remote';
// config.database.credentials.password = 'new';
// config.server.allowedOrigins.push('other');
Practical Examples
Example 1: Auto-Generate Form Types from API Schema
// Server schema type
interface CreateUserRequest {
username: string;
email: string;
password: string;
age: number;
acceptTerms: boolean;
}
// Form state: all fields are string (raw input), plus error messages
type FormValues<T> = {
[K in keyof T]: string;
};
type FormErrors<T> = {
[K in keyof T]?: string;
};
type FormTouched<T> = {
[K in keyof T]?: boolean;
};
interface FormState<T> {
values: FormValues<T>;
errors: FormErrors<T>;
touched: FormTouched<T>;
isSubmitting: boolean;
}
type CreateUserFormState = FormState<CreateUserRequest>;
// Helper to create the initial form state
function createInitialFormState<T>(keys: (keyof T)[]): FormState<T> {
const values = keys.reduce((acc, key) => {
(acc as any)[key] = '';
return acc;
}, {} as FormValues<T>);
return {
values,
errors: {},
touched: {},
isSubmitting: false,
};
}
// Usage
const formState = createInitialFormState<CreateUserRequest>([
'username', 'email', 'password', 'age', 'acceptTerms'
]);
Example 2: Permission-Based Property Access Control
type PermissionLevel = 'read' | 'write' | 'admin';
// Map each property to a permission level
type PermissionMap<T> = {
[K in keyof T]: PermissionLevel;
};
type FilterByPermission<T, M extends PermissionMap<T>, P extends PermissionLevel> = {
[K in keyof T as M[K] extends P ? K : never]: T[K];
};
interface Document {
id: number;
title: string;
content: string;
authorId: number;
createdAt: Date;
deletedAt: Date | null;
}
type DocumentPermissions = PermissionMap<Document>;
const documentPermissions: DocumentPermissions = {
id: 'read',
title: 'read',
content: 'read',
authorId: 'read',
createdAt: 'read',
deletedAt: 'admin',
};
// Fields visible only to admins
type AdminOnlyFields = FilterByPermission<Document, typeof documentPermissions, 'admin'>;
// { deletedAt: Date | null; }
Example 3: State Management — Auto-Generate Actions
// Auto-generate update action types from a state type
type UpdateActions<T, Prefix extends string> = {
[K in keyof T as `${Prefix}/${string & K}`]: {
type: `${Prefix}/${string & K}`;
payload: T[K];
};
}[keyof { [K in keyof T as `${Prefix}/${string & K}`]: unknown }];
interface CartState {
items: string[];
total: number;
couponCode: string | null;
isCheckingOut: boolean;
}
// Automatically generates: cart/items, cart/total, cart/couponCode, cart/isCheckingOut
type CartActions = UpdateActions<CartState, 'cart'>;
// Generic reducer factory
type StateReducer<S> = {
[K in keyof S]: (state: S, payload: S[K]) => S;
};
function createReducer<S>(handlers: StateReducer<S>) {
return function reducer(state: S, action: { type: keyof S; payload: any }): S {
const handler = handlers[action.type];
if (handler) {
return handler(state, action.payload);
}
return state;
};
}
Pro Tips
Tip 1: Debugging Mapped Types — Step-by-Step Expansion
// How to inspect a complex mapped type step by step
interface ComplexType {
a: string;
b: number | null;
c?: boolean;
readonly d: Date;
}
// Step 1: check the keyof result
type Keys = keyof ComplexType; // 'a' | 'b' | 'c' | 'd'
// Step 2: check indexed access types
type AType = ComplexType['a']; // string
type BType = ComplexType['b']; // number | null
// Step 3: inspect intermediate mapped type results
// Hovering over type MappedType = { [K in keyof ComplexType]: ... }
// in your IDE will show the expanded result.
// Break it into single-case helpers
type SingleMapping<T, K extends keyof T> = T[K] extends null | undefined
? never
: T[K];
type BResult = SingleMapping<ComplexType, 'b'>; // number (null removed)
Tip 2: Watch Out for the Empty Object Type
// {} means "any value except null and undefined"
// It is NOT the same as Record<string, never>!
type Empty = {};
const a: Empty = 42; // OK — number is assignable to {}
const b: Empty = 'hello'; // OK — string too
const c: Empty = { x: 1 }; // OK — objects too
// const d: Empty = null; // Error!
// To represent a truly empty object:
type TrulyEmpty = Record<string, never>; // or
type EmptyObject = { [key: string]: never };
// If all keys in a mapped type become never, the result looks like {}
type NeverMapped = {
[K in 'a' | 'b' as never]: string;
}; // Equivalent to {} — be careful: this is an empty object type
Tip 3: Conditional Key Distribution
// Extract keys that hold a specific value type
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
interface Mixed {
id: number;
name: string;
email: string;
age: number;
active: boolean;
createdAt: Date;
}
type StringKeys = KeysOfType<Mixed, string>; // 'name' | 'email'
type NumberKeys = KeysOfType<Mixed, number>; // 'id' | 'age'
type BooleanKeys = KeysOfType<Mixed, boolean>; // 'active'
// Select only the properties of a given type
type PickByType<T, U> = Pick<T, KeysOfType<T, U>>;
type StringFields = PickByType<Mixed, string>;
// { name: string; email: string; }
Summary Table
| Feature | Syntax | Description |
|---|---|---|
| Basic mapping | [K in keyof T]: T[K] | Iterate over all keys of T to produce a new type |
| Add optional | [K in keyof T]?: | Make all properties optional |
| Remove optional | [K in keyof T]-?: | Remove all optional markers |
| Add readonly | readonly [K in keyof T]: | Make all properties read-only |
| Remove readonly | -readonly [K in keyof T]: | Remove readonly (Mutable) |
| Key remapping | [K in keyof T as NewKey]: | Transform key names with an as clause |
| Key filtering | as T[K] extends X ? K : never | Remove keys with never |
| Conditional values | T[K] extends X ? A : B | Transform value types conditionally |
| Partial | [K in keyof T]?: T[K] | All properties optional |
| Required | [K in keyof T]-?: T[K] | All optional markers removed |
| Readonly | readonly [K in keyof T]: T[K] | All properties readonly |
| Pick | [K in Keys]: T[K] | Select specific keys |
| Record | [K in Keys]: V | Key-value pair generation |
Next we will look at Template Literal Types (6.4). You will learn the `${T}` syntax for combining and transforming string types, the built-in string utilities (Capitalize, Uppercase, etc.), and practical patterns like auto-generating event handler names.