Featured image of post Transforming Types dynamically using Mapped Types Featured image of post Transforming Types dynamically using Mapped Types

Transforming Types dynamically using Mapped Types

Craft dynamic types by mapping over keys of existing interfaces, utilizing key remapping and modifiers.

Introduction

Mapped types let you transform an existing object type by iterating over its keys. Think of them as Array.prototype.map() but at the type level. The syntax { [K in keyof T]: T[K] } creates a new type by applying a transformation to each property of T. This is how many of TypeScript’s built-in utility types work under the hood.


Basic Mapped Type

The simplest mapped type creates a clone of an existing type:

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

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

type UserClone = Clone<User>;
// { name: string; age: number }

Adding Modifiers

Mapped types can apply three modifiers: readonly, ? (optional), and - (remove):

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

// Make all properties optional
type MyPartial<T> = { [K in keyof T]?: T[K] };

// Remove readonly (the - modifier)
type MyMutable<T> = { -readonly [K in keyof T]: T[K] };

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

These are exactly how the built-in Readonly<T>, Partial<T>, and Required<T> are implemented.


Filtering with as Key Remapping

TypeScript 4.1 introduced key remapping using the as clause. This allows filtering or renaming keys:

// Filter keys that are strings
type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

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

type OnlyStrings = StringKeys<User>;
// { name: string; email: string }

Template Literal Key Changes

Combine mapped types with template literal types to transform key names:

// Add a prefix to all keys
type WithPrefix<T, P extends string> = {
  [K in keyof T as `${P}_${K & string}`]: T[K];
};

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

type Prefixed = WithPrefix<User, 'user'>;
// { user_name: string; user_age: number }

To convert to getter/setter conventions:

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

type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }

Pick and Omit Implementations

Here’s how Pick and Omit work internally:

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type MyOmit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

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

type NameOnly = MyPick<User, 'name'>;
// { name: string }

type NoEmail = MyOmit<User, 'email'>;
// { name: string; age: number }

Practical Patterns

Deep Partial (recursive mapped type)

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

Nullable Properties

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

Rename Keys with a Mapping

type RenameKeys<T, M extends Record<string, string>> = {
  [K in keyof T as K extends keyof M ? M[K] : K]: T[K];
};

Conclusion

Mapped types are the backbone of TypeScript’s type transformation capabilities. From basic modifiers (readonly, ?) to advanced key remapping with template literals, they give you fine-grained control over how types evolve. Combined with conditional types, they enable the kind of compile-time metaprogramming that makes TypeScript truly powerful.