TypeScript Mapped Types

Mapped Types are a powerful feature in TypeScript for creating new types based on existing types.

They allow developers to modify property characteristics in batch, such as making all properties optional or all properties readonly.

Mapped Types are the core technology for implementing TypeScript's built-in utility types.


SVG Diagram: How Mapped Types Work Background Title How Mapped Types Work Original Type Original Type interface User { id: number; name: string; } Arrow Mapping syntax Mapping syntax { [P in keyof T]?: T[P] // Add optional modifier } Arrow Result Type Result Type type PartialUser = { id?: number; name?: string; } Lower section: Modifier description Mapped Type Modifiers Modifier Description ? prefix Add optional modifier type Partial<T> readonly prefix Add readonly modifier type Readonly<T> -? prefix Remove optional modifier type Required<T> as keyword Remap key names as `${P}` Arrow marker

Why Need Mapped Types

In practical TypeScript development, we often need to create variants based on existing types.

For example, a version where all properties are optional, or a version where all properties are readonly.

The traditional way is to define these types manually, which is tedious and error-prone.

Mapped types provide a declarative way to automatically generate these type variants.

Concept explanation:Mapped types usekeyofandinthe keyword to iterate over all keys of an existing type, then apply the same type transformation to each key.


Basic Mapped Types

Basic mapped types create new types by iterating over all keys of the original type.

This is the foundation for implementing utility types like Partial and Readonly.

Example

// Define user interface
interface User {
    // User ID
    id: number;
    // Username
    name: string;
    // User email
    email: string;
}

// Implement the Partial utility type
// Iterate over all keys of T, add optional modifier ?
type Partial<T> = {
    // P iterates over all keys returned by keyof T
    // T[P] obtains the value type of the corresponding key in the original type
    [P in keyof T]?: T[P];
};

// Use the Partial type
type PartialUser = Partial<User>;

// The PartialUser type is equivalent to:
// { id?: number; name?: string; email?: string }

// Only partial properties can be provided
var user: PartialUser = { name: "Alice" };

console.log("Partial user: " + JSON.stringify(user));

Output:

部分用户: {"name":"Alice"}

Syntax explanation:[P in keyof T]Indicates iterating over all keys of type T.?The modifier makes properties optional.


Property Modifiers

Mapped types support multiple property modifiers to change property characteristics.

These modifiers can be combined to meet different type transformation needs.

Example

// Define user interface
interface User {
    // Username
    name: string;
    // User age
    age: number;
}

// Use readonly mapping: make all properties readonly
// Add readonly modifier
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// Use optional mapping: make all properties optional (basic version)
type Optional<T> = {
    [P in keyof T]?: T[P];
};

// Use -? mapping: remove the optional modifier (make required)
// -? removes the original ? modifier
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// Test readonly type
var readonlyUser: Readonly<User> = { name: "Alice", age: 25 };
// readonlyUser.age = 30; // Error: readonly property cannot be modified

// Test optional type
var optionalUser: Optional<User> = { name: "Bob" };

console.log("Readonly: " + JSON.stringify(readonlyUser));
console.log("Optional: " + JSON.stringify(optionalUser));

Output:

只读: {"name":"Alice","age":25}
可选: {"name":"Bob"}

Modifier description:?adds optional,-?removes optional;readonlyadds readonly,-readonlyremoves readonly.


Key Remapping

Mapped types can also useasthe keyword to remap key names.

This is useful when you need to uniformly modify key name formats.

Example

// Define user interface
interface User {
    // User ID
    id: number;
    // Username
    name: string;
    // User age
    age: number;
}

// Use the as keyword to remap key names
// Add a prefix to all keys
type WithPrefix<T, Prefix extends string> = {
    // Use template literal types to rename keys
    // Capitalize makes the first letter uppercase
    [P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};

// Use WithPrefix to add the "user" prefix
type PrefixedUser = WithPrefix<User, "user">;

// Converted type:
// { userId: number; userName: string; userAge: number }

// Use the type with prefix
var user: PrefixedUser = { userId: 1, userName: "Alice", userAge: 25 };

console.log("With prefix: " + JSON.stringify(user));

Output:

带前缀: {"userId":1,"userName":"Alice","userAge":25}

Template literal types:Use`${Prefix}${Capitalize}`can dynamically generate new key names.


Key Filtering

By combining conditional types and mapped types, key filtering can be achieved.

This is very useful when implementing utility types like Omit.

Example

// Define user interface
interface User {
    // User ID
    id: number;
    // Username
    name: string;
    // User password
    password: string;
    // User email
    email: string;
}

// Implement Omit: exclude specified keys
// Use conditional types to filter keys
type Omit<T, K extends keyof T> = {
    // P iterates over all keys of T
    // If P is assignable to K (i.e., in the exclusion list), return never (excluded)
    // Otherwise return P (keep the key)
    [P in keyof T as P extends K ? never : P]: T[P];
};

// Use Omit to exclude the password key
type UserWithoutPassword = Omit<User, "password">;

// Converted type:
// { id: number; name: string; email: string }

// Use the type after excluding password
var user: UserWithoutPassword = { id: 1, name: "Alice", email: "[email protected]" };

console.log("Without password: " + JSON.stringify(user));

Output:

无密码: {"id":1,"name":"Alice","email":"[email protected]"}

never type:In mapped types, using never as the property type causes the property to be completely removed.


Conditional Mapping

Mapped types can be combined with conditional types to apply different transformations based on property types.

This makes type transformation more flexible and intelligent.

Example

// Define API response interface
interface APIResponse {
    // Response data
    data: string;
    // Error message
    error: string;
    // Whether loading
    isLoading: boolean;
    // Timestamp
    timestamp: number;
}

// Convert function types to () => void
// Iterate over all properties and perform conditional conversion based on property type
type FunctionToVoid<T> = {
    // If T[P] is a function type, convert it to () => void
    // Otherwise keep the original type unchanged
    [P in keyof T]: T[P] extends (...args: any[]) => any
        ? () => void
        : T[P];
};

// Use conditional mapping
var response: FunctionToVoid<APIResponse> = {
    data: "hello",
    error: "",
    isLoading: false,
    timestamp: Date.now()
};

console.log("Response: " + JSON.stringify(response));

Output:

响应: {"data":"hello","error":"","isLoading":false,"timestamp":...}

Application scenarios:Conditional mapping is often used in scenarios where different handling is needed based on types, such as processing API responses or cleaning up configuration objects.


Built-in Mapped Types

TypeScript has many built-in utility types implemented based on mapped types.

These utility types can satisfy most daily development needs.

Example

// Partial - makes all properties optional
type P1 = Partial<{ a: string; b: number }>;
// Result: { a?: string; b?: number }

// Required - makes all optional properties required
type R1 = Required<{ a?: string; b?: number }>;
// Result: { a: string; b: number }

// Readonly - makes all properties readonly
type RO1 = Readonly<{ a: string; b: number }>;
// Result: { readonly a: string; readonly b: number }

// Pick - Select specified properties
type PK = Pick<{ a: string; b: number; c: boolean }, "a" | "b">;
// Result: { a: string; b: number }

// Omit - Exclude specified properties
type OM = Omit<{ a: string; b: number; c: boolean }, "c">;
// Result: { a: string; b: number }

// Test Partial
console.log("Partial: " + JSON.stringify({} as P1));

// Test Pick
console.log("Pick: " + JSON.stringify({ a: "x" } as PK));

Utility type combinations:These built-in utility types are all implemented based on mapped types and conditional types. Understanding how they work can help you use them better.


Notes

  • keyof keyword:Used to obtain a union type composed of all keys of a type.
  • in keyword:Used to iterate over union types of key names.
  • Modifier position:?andreadonlyBefore the property name, indicates adding a modifier.
  • Minus modifier:-?and-readonlyUsed to remove modifiers.
  • as keyword:Used to remap key names; must return a string or numeric literal type.

Advanced:Mapped types can be combined with conditional types and template literal types to implement complex type transformations.


Summary

Mapped types are one of the most powerful features in the TypeScript type system.

  • keyof:Get all keys of a type
  • in:Iterate over key names for mapping
  • ?:Add optional modifiers
  • readonly:Add readonly modifiers
  • -?:Remove optional modifiers
  • as:Remap key names

Best practices:Using mapped types wisely can greatly reduce repetitive type definitions and improve code maintainability.


Other extensions