Back to Blog
Lesson 40 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 27, 20264 min read

Mastering Mapped Types in TypeScript: Dynamic Type Transformation

Mapped types are the key to building flexible, DRY TypeScript applications. Learn how to transform types dynamically and simplify your API client logic.

TypeScriptGenericsMapped TypesAdvanced TypesAPI Client
A vintage typewriter displaying the words 'Edge Computing' on paper, highlighting technological contrast.

Previously in this course, we explored Using keyof for Dynamic Access to safely look up object properties. Now, we're taking a significant leap forward by using that same logic to generate entire new types on the fly.

Understanding Mapped Types from First Principles

In TypeScript, a Mapped Type is a generic type that uses a union of keys (usually created via keyof) to iterate over and transform the properties of an existing type. Think of it like a map function for your data structures, but instead of transforming values at runtime, you are transforming their definitions at compile-time.

The syntax relies on the bracket notation: [P in K]: T.

  • P: The type variable representing the current key.
  • K: The union of keys to iterate over.
  • T: The new type to assign to that property.

This is the ultimate tool for reducing duplication in your codebase. Instead of manually defining a ReadonlyTask or a TaskUpdate interface, you can generate them automatically.

Worked Example: Creating a Read-Only Utility

Let’s apply this to our ongoing project: the task API client. We have a Task interface, but sometimes we want to ensure our UI components don't accidentally mutate task objects retrieved from the server.

TYPESCRIPT
interface Task {
  id: string;
  title: string;
  status: CE9178">'pending' | CE9178">'completed';
}

// A custom Mapped Type to make all properties readonly
type ReadonlyTask<T> = {
  readonly [P in keyof T]: T[P];
};

const myTask: ReadonlyTask<Task> = {
  id: CE9178">'1',
  title: CE9178">'Learn Mapped Types',
  status: CE9178">'pending'
};

// myTask.title = CE9178">'New Title'; // Error: Cannot assign to CE9178">'title' because it is a read-only property.

By defining ReadonlyTask<T>, we created a reusable transformation. TypeScript iterates over every key in T, looks up the original type using indexed access T[P], and prepends the readonly modifier.

Implementing Dynamic Transformations

Mapped types also support "modifiers" that let you change the nature of the properties, not just their types. You can add readonly or make properties optional by adding a ? prefix.

Here is how we can use this to create a PartialTask for our API PATCH requests:

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

// Now we can pass an object with only the fields we want to update
const updates: OptionalTask<Task> = {
  status: CE9178">'completed'
};

This is the underlying mechanism for built-in utility types like Partial<T> and Readonly<T>. Understanding this allows you to build sophisticated domain-specific utilities, similar to the strategies discussed in TypeScript Data Transformation: Mastering Mapped Types for API Models.

Hands-on Exercise

Refactor our Task client's "Update" functionality. Create a RequiredTask mapped type that takes a type with optional properties (like our OptionalTask above) and removes the ? modifier to force all fields to be present.

Hint: You can remove modifiers by prefixing them with a minus sign, like -readonly or -?.

Common Pitfalls

  1. Over-Engineering: Don't create complex mapped types for simple, static interfaces. If you only have one Task type, just write the interface. Mapped types shine when you have a library of models.
  2. Losing Type Information: Be careful when using mapped types with complex types like unions. Sometimes, the mapping can unintentionally strip or widen specific types if not handled with care.
  3. Readability: While powerful, they can make code harder for beginners to parse. Always document your custom utility types with a brief comment explaining their intent.

FAQ

Are Mapped Types the same as Generics? They are related, but distinct. Generics provide a "template" for a type, while Mapped Types provide the "logic" to iterate over the keys of that generic input.

When should I use a Mapped Type over an Interface? Use an interface when you know the shape of the data upfront. Use a Mapped Type when the shape is derived from another type and needs to remain in sync automatically.

Can I rename keys while mapping? Yes, using "Key Remapping" (introduced in newer TS versions), you can use the as keyword to transform the key itself: [P in keyof T as Capitalize<string & P>]: T[P].

Recap

Mapped types allow us to transform existing types dynamically, ensuring our code remains DRY and type-safe. By using [P in keyof T], we can programmatically modify properties, add modifiers, or even transform keys, which is essential for scaling complex API clients. For further exploration of how these concepts interact with runtime logic, see Mastering TypeScript Conditional Types: A Guide to Dynamic Transformation.

Up next: We will dive into Conditional Types to add "if-else" logic to our type definitions.

Similar Posts