TypeScript Mapped Types: Transforming Types

Mapped types are TypeScript's mechanism for transforming one type into another by iterating over its properties. They're the foundation of utility types like `Partial<T>`, `Readonly<T>`, and `Pick<T,...

Key Insights

  • Mapped types transform existing types by iterating over their properties, enabling you to create type-safe utilities without code duplication
  • The as clause unlocks advanced key remapping, letting you filter properties, rename keys, and generate entirely new property names based on the original type structure
  • Combining mapped types with conditional types and template literals creates powerful transformations for real-world scenarios like API contracts, form handling, and event systems

Introduction to Mapped Types

Mapped types are TypeScript’s mechanism for transforming one type into another by iterating over its properties. They’re the foundation of utility types like Partial<T>, Readonly<T>, and Pick<T, K>, and understanding them unlocks the ability to create sophisticated type transformations tailored to your application’s needs.

The basic syntax uses an index signature with the in keyword: { [K in keyof T]: ... }. This iterates over each key K in type T, allowing you to transform the property type or modify the key itself.

Here’s how you’d recreate TypeScript’s built-in Partial<T>:

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = MyPartial<User>;
// Result: { id?: number; name?: string; email?: string; }

The ? modifier makes each property optional. This single line replaces what would otherwise require manually defining a new interface with optional properties—and it automatically updates when User changes.

Basic Mapped Type Patterns

Mapped types support several modifiers that control property behavior. The readonly modifier prevents reassignment, while ? makes properties optional. You can also add or remove these modifiers using + and - prefixes.

Here’s a custom Readonly<T> implementation:

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

type ReadonlyUser = MyReadonly<User>;
// All properties become readonly

You can remove modifiers too. The Required<T> utility removes optional modifiers:

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

interface PartialConfig {
  apiUrl?: string;
  timeout?: number;
}

type CompleteConfig = MyRequired<PartialConfig>;
// Result: { apiUrl: string; timeout: number; }

Converting all properties to a specific type is another common pattern:

type Stringify<T> = {
  [K in keyof T]: string;
};

type UserStrings = Stringify<User>;
// Result: { id: string; name: string; email: string; }

This pattern is useful for form inputs where all values are initially strings, or for creating mock data structures.

Key Remapping with as

The as clause, introduced in TypeScript 4.1, revolutionized mapped types by enabling key transformation. You can rename properties, filter them out, or generate entirely new key names.

Prefixing property names creates getter-style interfaces:

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

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

The string & K intersection ensures we’re only working with string keys, as Capitalize requires a string.

You can filter properties by returning never for keys you want to exclude:

type RemoveIdFields<T> = {
  [K in keyof T as K extends `${string}Id` ? never : K]: T[K];
};

interface Product {
  productId: number;
  name: string;
  categoryId: number;
  price: number;
}

type ProductWithoutIds = RemoveIdFields<Product>;
// Result: { name: string; price: number; }

Here’s a practical example converting snake_case to camelCase:

type SnakeToCamel<S extends string> = 
  S extends `${infer First}_${infer Rest}`
    ? `${First}${SnakeToCamel<Capitalize<Rest>>}`
    : S;

type CamelCaseKeys<T> = {
  [K in keyof T as SnakeToCamel<string & K>]: T[K];
};

interface ApiResponse {
  user_id: number;
  first_name: string;
  last_name: string;
}

type CamelResponse = CamelCaseKeys<ApiResponse>;
// Result: { userId: number; firstName: string; lastName: string; }

Template Literal Types in Mapped Types

Template literal types combine with mapped types to generate related property names dynamically. This is particularly powerful for creating type-safe getters, setters, and event handlers.

Generating both 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>;

type UserAccessors = GettersAndSetters<User>;
// Result includes getId(), setId(value: number), getName(), setName(value: string), etc.

Event handlers benefit from this pattern:

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

interface FormFields {
  email: string;
  password: string;
  rememberMe: boolean;
}

type FormHandlers = EventHandlers<FormFields>;
// Result: {
//   onEmailChange: (value: string) => void;
//   onPasswordChange: (value: string) => void;
//   onRememberMeChange: (value: boolean) => void;
// }

Creating REST API endpoint types from resource definitions:

type ApiEndpoints<T extends string> = {
  [K in T as `get${Capitalize<K>}`]: () => Promise<any>;
  [K in T as `create${Capitalize<K>}`]: (data: any) => Promise<any>;
  [K in T as `update${Capitalize<K>}`]: (id: string, data: any) => Promise<any>;
  [K in T as `delete${Capitalize<K>}`]: (id: string) => Promise<void>;
};

type Resources = 'user' | 'product' | 'order';
type API = ApiEndpoints<Resources>;
// Generates getUser, createUser, updateUser, deleteUser, getProduct, etc.

Conditional Types within Mapped Types

Combining conditional types with mapped types enables property-specific transformations based on type checking.

Making only string properties nullable:

type NullableStrings<T> = {
  [K in keyof T]: T[K] extends string ? T[K] | null : T[K];
};

interface Article {
  id: number;
  title: string;
  content: string;
  published: boolean;
}

type ArticleWithNullableStrings = NullableStrings<Article>;
// Result: { id: number; title: string | null; content: string | null; published: boolean; }

Converting Date objects to ISO strings for serialization:

type SerializeDate<T> = {
  [K in keyof T]: T[K] extends Date ? string : T[K];
};

interface Event {
  id: string;
  name: string;
  startDate: Date;
  endDate: Date;
  capacity: number;
}

type SerializedEvent = SerializeDate<Event>;
// Result: { id: string; name: string; startDate: string; endDate: string; capacity: number; }

Deep transformations require recursive mapped types:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
}

type ImmutableConfig = DeepReadonly<Config>;
// All nested properties become readonly

Real-World Use Cases

Form state management benefits significantly from mapped types. Convert data models to form field types:

type FormField<T> = {
  value: T;
  error: string | null;
  touched: boolean;
};

type FormState<T> = {
  [K in keyof T]: FormField<T[K]>;
};

interface RegistrationData {
  email: string;
  password: string;
  age: number;
}

type RegistrationForm = FormState<RegistrationData>;
// Result: {
//   email: { value: string; error: string | null; touched: boolean; };
//   password: { value: string; error: string | null; touched: boolean; };
//   age: { value: number; error: string | null; touched: boolean; };
// }

API response transformations ensure type safety across your application layers:

type ApiResponse<T> = {
  [K in keyof T]: T[K] extends Date ? string : T[K];
};

type ApiRequest<T> = {
  [K in keyof T as K extends 'id' ? never : K]: T[K];
};

interface User {
  id: number;
  name: string;
  createdAt: Date;
}

type CreateUserRequest = ApiRequest<User>;
// Result: { name: string; createdAt: Date; } - id removed

type UserResponse = ApiResponse<User>;
// Result: { id: number; name: string; createdAt: string; } - Date serialized

Type-safe Redux action creators leverage mapped types:

type ActionCreators<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (payload: T[K]) => {
    type: `SET_${Uppercase<string & K>}`;
    payload: T[K];
  };
};

interface AppState {
  user: User | null;
  loading: boolean;
  error: string | null;
}

type Actions = ActionCreators<AppState>;
// Generates setUser, setLoading, setError with proper action types

Best Practices and Performance Considerations

Keep mapped types focused and composable. Instead of creating one massive transformation, break it into smaller utilities:

// Good: Composable utilities
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type NullableReadonly<T> = Readonly<Nullable<T>>;

// Avoid: Monolithic transformations
type ComplexTransform<T> = {
  readonly [K in keyof T as `prefixed_${string & K}`]: T[K] | null;
};

Name your mapped types descriptively. TransformUser tells you nothing; UserApiResponse or UserFormFields communicates intent.

Watch for circular references in recursive mapped types. TypeScript will error on infinitely recursive types, but the error messages can be cryptic. Always include a base case:

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object 
    ? T[K] extends Function 
      ? T[K] 
      : DeepPartial<T[K]>
    : T[K];
};

This checks for functions to prevent recursing into them, avoiding common pitfalls.

Mapped types can impact compilation performance with very large types or deeply nested transformations. If you notice slow IDE performance, consider simplifying your type transformations or using type aliases to cache intermediate results.

Mapped types are TypeScript’s most powerful feature for type-level programming. They eliminate boilerplate, enforce consistency, and catch errors at compile time. Master them, and you’ll write more maintainable, type-safe code with significantly less effort.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.