TypeScript Mapped Types: Stop Writing Repetitive Type Definitions

If you've ever found yourself copying and pasting type definitions with minor variations—making everything optional, adding readonly, or prefixing keys—you've hit the exact problem mapped types solve. This TypeScript feature transforms how you design type systems, turning dozens of lines of boilerplate into a single, maintainable declaration.

What Are Mapped Types?

Mapped types let you create new types by transforming the properties of an existing type. Think of them as the Array.map() of TypeScript's type system: you take one type, apply a transformation to each property, and get a new type out.

Here's the simplest example:

type User = {
  id: number;
  name: string;
  email: string;
};

// Without mapped types - repetitive!
type PartialUser = {
  id?: number;
  name?: string;
  email?: string;
};

// With mapped types - DRY!
type PartialUser = {
  [K in keyof User]?: User[K];
};

The syntax [K in keyof User] iterates over every key in User, and User[K] grabs the corresponding value type. The ? modifier makes each property optional.

Real-World Use Cases

Making Properties Readonly

Instead of manually adding readonly to every field:

type Config = {
  apiUrl: string;
  timeout: number;
  retries: number;
};

type ReadonlyConfig = {
  readonly [K in keyof Config]: Config[K];
};

// TypeScript actually provides this built-in as Readonly<T>
const config: Readonly<Config> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only property

Creating API Response Types

When your API returns data wrapped in a common structure:

type ApiResponse<T> = {
  [K in keyof T]: {
    value: T[K];
    lastUpdated: Date;
  };
};

type Product = {
  name: string;
  price: number;
};

type ProductResponse = ApiResponse<Product>;
// Result:
// {
//   name: { value: string; lastUpdated: Date };
//   price: { value: number; lastUpdated: Date };
// }

Filtering Properties by Type

Extract only the string properties from a type:

type User = {
  id: number;
  name: string;
  email: string;
  age: number;
  bio: string;
};

type StringPropertiesOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type UserStrings = StringPropertiesOnly<User>;
// Result: { name: string; email: string; bio: string; }

The as clause lets you remap or filter keys. K : never effectively removes that property from the resulting type.

Advanced Patterns: Template Literal Types

TypeScript 4.1+ lets you combine mapped types with template literal types for powerful key transformations.

Creating Getter Functions

type User = {
  name: string;
  age: number;
  email: string;
};

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

type UserGetters = Getters<User>;
// Result:
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

Event Handler Types

Generate event handler names from state properties:

type FormState = {
  username: string;
  password: string;
  remember: boolean;
};

type FormHandlers<T> = {
  [K in keyof T as `handle${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};

type FormCallbacks = FormHandlers<FormState>;
// Result:
// {
//   handleUsernameChange: (value: string) => void;
//   handlePasswordChange: (value: string) => void;
//   handleRememberChange: (value: boolean) => void;
// }

Combining Multiple Transformations

You can stack mapped types for complex transformations:

type APIEndpoints = {
  users: { id: number; name: string }[];
  posts: { id: number; title: string }[];
  comments: { id: number; text: string }[];
};

// Create loading and error states for each endpoint
type AsyncState<T> = {
  [K in keyof T as `${string & K}Loading`]: boolean;
} & {
  [K in keyof T as `${string & K}Error`]: Error | null;
} & {
  [K in keyof T as `${string & K}Data`]: T[K] | null;
};

type AppState = AsyncState<APIEndpoints>;
// Result:
// {
//   usersLoading: boolean;
//   usersError: Error | null;
//   usersData: { id: number; name: string }[] | null;
//   postsLoading: boolean;
//   postsError: Error | null;
//   postsData: { id: number; title: string }[] | null;
//   ... same for comments
// }

Built-in Utility Types You Already Know

TypeScript's standard utility types are actually mapped types under the hood:

  • Partial<T> - Makes all properties optional
  • Required<T> - Makes all properties required
  • Readonly<T> - Makes all properties readonly
  • Pick<T, K> - Selects specific properties
  • Omit<T, K> - Excludes specific properties
  • Record<K, T> - Creates an object type with specific keys

Understanding mapped types means you can read TypeScript's source code and create your own custom utilities.

Key Takeaways

Mapped types eliminate repetitive type definitions by programmatically transforming existing types. They're essential when:

  • You need variations of the same type (optional, readonly, nullable)
  • You're generating types from other types (API wrappers, event handlers)
  • You want to enforce consistent patterns across your codebase
  • You're building reusable type utilities

The syntax [K in keyof T] is your iterator, T[K] grabs the value type, and the as clause lets you transform keys. Combine these with template literal types for maximum flexibility.

Start replacing repetitive type code today—your future self will thank you when requirements change and you only need to update one source type instead of five variations.