TypeScript Mapped Types: A Complete Guide

TypeScript mapped types are one of the most powerful features for type transformation and manipulation. This guide will help you understand how to use mapped types effectively to create flexible and reusable type definitions.

Mapped types allow you to create new types based on existing ones by transforming each property according to a rule you define. Think of them as array map() operations but for types instead of values.

Table of Contents

What are Mapped Types?

Mapped types build new types by iterating over existing ones and applying transformations. The syntax might look intimidating at first, but we’ll break it down step by step.

type ReadonlyVersion<T> = {
  readonly [P in keyof T]: T[P];
};
Code language: HTML, XML (xml)

This simple example creates a new type where all properties are readonly. Let’s understand how it works.

Basic Syntax of Mapped Types

The basic syntax follows this pattern:

type NewType<T> = {
  [P in keyof T]: TransformedType
};
Code language: HTML, XML (xml)

Let’s break down each part:

  • NewType<T>: The name of our mapped type with a generic parameter
  • [P in keyof T]: Iteration over all properties in T
  • TransformedType: The type transformation we want to apply

Common Use Cases

Making Properties Optional

type Partial<T> = {
  [P in keyof T]?: T[P];
};

interface User {
  name: string;
  age: number;
}

// All properties become optional
type PartialUser = Partial<User>;
Code language: PHP (php)

Making Properties Required

type Required<T> = {
  [P in keyof T]-?: T[P];
};

interface Config {
  debug?: boolean;
  cache?: boolean;
}

// All properties become required
type RequiredConfig = Required<Config>;
Code language: PHP (php)

Advanced Transformations

Property Name Transformations

You can transform property names using template literal types:

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface Person {
  name: string;
  age: number;
}

// Creates: { getName: () => string, getAge: () => number }
type PersonGetters = Getters<Person>;
Code language: JavaScript (javascript)

Conditional Type Transformations

Combine mapped types with conditional types for more complex transformations:

type NonNullableProps<T> = {
  [P in keyof T]: NonNullable<T[P]>;
};

interface UserData {
  name: string | null;
  age: number | undefined;
}

// Removes null and undefined from all properties
type RequiredUserData = NonNullableProps<UserData>;
Code language: PHP (php)

Best Practices

1. Keep Transformations Simple

Avoid complex nested transformations that make your types hard to understand:

// Good
type ReadonlyDeep<T> = {
  readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P];
};

// Avoid complex nested transformations
type ComplexTransform<T> = {
  [P in keyof T]: T[P] extends object 
    ? { [K in keyof T[P]]: T[P][K] extends Array<infer U> 
      ? Array<ComplexTransform<U>> 
      : T[P][K] }
    : T[P];
};
Code language: JavaScript (javascript)

2. Use Built-in Mapped Types

TypeScript provides several built-in mapped types. Use them when possible instead of creating your own:

// Use built-in Partial
type PartialConfig = Partial<Config>;

// Instead of creating your own
type CustomPartial<T> = {
  [P in keyof T]?: T[P];
};
Code language: JavaScript (javascript)

3. Document Complex Transformations

Add comments explaining complex type transformations:

/**
 * Creates a new type where all properties are functions
 * that return the original property type
 */
type Functionalize<T> = {
  [P in keyof T]: () => T[P];
};
Code language: JavaScript (javascript)

Common Gotchas and Solutions

1. Handling Union Types

type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Union = { a: string } | { b: number };
type Intersection = UnionToIntersection<Union>; // { a: string } & { b: number }
Code language: JavaScript (javascript)

2. Preserving Modifiers

type PreserveModifiers<T> = {
  [P in keyof T]: T[P];
} & {};

interface Original {
  readonly id: number;
  optional?: string;
}

type Preserved = PreserveModifiers<Original>; // Keeps readonly and optional
Code language: PHP (php)

Integration with Other TypeScript Features

Mapped types work well with other TypeScript features like conditional types and template literal types. Here’s an example combining multiple features:

type PropertyTypes<T> = {
  [P in keyof T as `${string & P}Type`]: T[P] extends object
    ? 'object'
    : T[P] extends Function
    ? 'function'
    : 'primitive';
};

interface Example {
  name: string;
  age: number;
  data: object;
  handler: () => void;
}

// Creates: {
//   nameType: 'primitive';
//   ageType: 'primitive';
//   dataType: 'object';
//   handlerType: 'function';
// }
type Types = PropertyTypes<Example>;
Code language: JavaScript (javascript)

Conclusion

Mapped types are a powerful feature in TypeScript that allows for flexible type transformations. They’re especially useful when working with complex type systems and when you need to apply consistent transformations across multiple types.

For more TypeScript features, check out our guide on TypeScript Type Aliases which pairs well with mapped types for creating more maintainable type definitions.

Start simple with basic transformations and gradually move to more complex patterns as you become comfortable with the syntax. Remember to prioritize readability and maintainability in your type definitions.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap