Skip to main content
Advertisement

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, and Record work 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

FeatureSyntaxDescription
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 readonlyreadonly [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 filteringas T[K] extends X ? K : neverRemove keys with never
Conditional valuesT[K] extends X ? A : BTransform value types conditionally
Partial[K in keyof T]?: T[K]All properties optional
Required[K in keyof T]-?: T[K]All optional markers removed
Readonlyreadonly [K in keyof T]: T[K]All properties readonly
Pick[K in Keys]: T[K]Select specific keys
Record[K in Keys]: VKey-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.

Advertisement