TypeScript's type system is very powerful, and Mapped Types are one of its most practical features. They let you create new types based on existing ones, modifying property modifiers in bulk. This article covers mapped types from basics to real-world usage.
Basic Syntax
The core syntax of mapped types:
typescript
type MappedType<T> = {
[K in keyof T]: T[K];
};
keyof T: gets all keys of T as a union typeK in keyof T: iterates over each keyT[K]: gets the type for key K in T (indexed access type)
A simple example:
typescript
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Change all properties to string type
type StringifyUser = {
[K in keyof User]: string;
};
// Equivalent to:
// {
// id: string
// name: string
// email: string
// age: string
// }
Readonly Modifier: readonly +/-
+ adds a modifier (default, can be omitted), - removes it:
typescript
// Add readonly (all properties become readonly)
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// Remove readonly
type MutableUser = {
-readonly [K in keyof ReadonlyUser]: ReadonlyUser[K];
};
// TypeScript's built-in Readonly<T> is implemented this way:
// type Readonly<T> = {
// readonly [P in keyof T]: T[P]
// }
Optional Modifier: ? +/-
typescript
// All properties become optional
type PartialUser = {
[K in keyof User]?: User[K];
};
// Equivalent to built-in Partial<T>
// Remove optional (all properties become required)
type RequiredUser = {
[K in keyof PartialUser]-?: PartialUser[K];
};
// Equivalent to built-in Required<T>
Combining with Conditional Types
Mapped types combined with conditional types enable more precise type transformations:
typescript
// Extract only keys with function type values
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface Api {
baseUrl: string;
timeout: number;
getUsers: () => Promise<User[]>;
deleteUser: (id: number) => Promise<void>;
version: string;
}
type ApiFunctionKeys = FunctionKeys<Api>;
// "getUsers" | "deleteUser"
Practice: Implementing Common Utility Types
DeepPartial — Deep Optional
The built-in Partial only handles the first level:
typescript
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
ttl: number;
maxSize: number;
};
}
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepPartial<T[K]>
: T[K];
};
const partialConfig: DeepPartial<Config> = {
database: {
host: "localhost",
// port and credentials can be omitted
},
// cache can be omitted too
};
DeepReadonly — Deep Readonly
typescript
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
const config: DeepReadonly<Config> = getConfig();
config.database.host = "new-host"; // Error: readonly
Record Implementation
typescript
// Built-in Record<K, V>: maps union type K to value type V
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
type UserRoles = MyRecord<"admin" | "user" | "guest", boolean>;
// { admin: boolean; user: boolean; guest: boolean }
Nullable
typescript
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
// {
// id: number | null
// name: string | null
// email: string | null
// age: number | null
// }
Summary
- Mapped types transform existing types in bulk, avoiding repetitive type declarations
+readonly/-readonlyand+?/-?control readonly and optional modifiers- Combined with conditional types, mapped types become very powerful
- TypeScript's built-in
Partial,Required,Readonly,Record,Pick,Omitare all implemented using mapped types
