14.1 Mapped Types: Transforming Every Property of a Type
Let’s talk about Mapped Types, which are essentially your way of telling TypeScript, “Hey, I want to take this existing type and systematically transform every single property in it.” It’s like a factory assembly line for your types. You provide the blueprint (the original type) and the modification instructions (the transformation), and TypeScript cranks out a brand new, transformed type. This is where you move from merely describing your data to actively shaping it with code.
The syntax looks a bit funky at first, but it’s incredibly logical once you break it down. It uses the power of the in keyword inside a type declaration to iterate over the keys of another type.
type OldType = {
readonly id: number;
name: string;
age?: number;
};
// Let's make a new type where every property is mutable and optional
type NewType = {
[K in keyof OldType]?: OldType[K];
};
// This is equivalent to:
// {
// id?: number;
// name?: string;
// age?: number;
// }
In this snippet, K becomes a placeholder for each key in OldType ("id", "name", "age"). For each K, we’re saying the new type will have the same key, but we’re adding the ? modifier to make it optional. The value type remains OldType[K] (a lookup of the property type for that key).
The Basic Building Blocks: in and keyof
The magic hinges on two operators working in tandem. The keyof T operator gives you a union type of all the keys in T. Think of it as getting a list of all the property names. The in keyword then lets you loop over each member of that union. It’s a for...in loop, but for the type system. Without this combination, you’d be hand-writing every transformation, which defeats the whole purpose.
Modifiers: Adding and Removing readonly and ?
This is where mapped types become genuinely powerful. You can explicitly add or remove the readonly and optional (?) modifiers. This is done with the + and - operators. If you don’t specify one, it defaults to +, but being explicit is always clearer.
// Make all properties mutable and required (remove readonly and ?)
type Concrete<T> = {
-readonly [K in keyof T]-?: T[K];
};
// Make all properties read-only and optional
type Flexible<T> = {
+readonly [K in keyof T]+?: T[K];
};
interface User {
readonly id: number;
name?: string;
}
type ConcreteUser = Concrete<User>;
// Equivalent to { id: number; name: string; }
type FlexibleUser = Flexible<User>;
// Equivalent to { readonly id?: number; readonly name?: string; }
The fact that you can remove readonly-ism and optionality is crucial. It allows you to take a lax type (e.g., something from an API response where everything is optional) and transform it into a type that must be present for your internal application logic.
Real-World Example: Partial and Readonly
You’ve probably used these utility types without realizing they are mapped types. Let’s demystify them right now. They are not magic; they are just brilliantly simple applications of this concept.
// Built-in TypeScript definitions (simplified)
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
See? Nothing up their sleeves. Partial adds a ? to every property. Required removes the ? from every property (-?). Readonly adds the readonly modifier. Now you know exactly how they work and can even create your own variants.
Key Remapping with as (TypeScript 4.1+)
This was a game-changer. Before TypeScript 4.1, you could only transform the value of a property, not its key. The as clause in a mapped type lets you leverage template literal types to transform the key names themselves. This is how you get utilities like Uppercase<StringLiteral>.
// Prefix every key with "get" and capitalize the original key
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// Equivalent to:
// {
// getName: () => string;
// getAge: () => number;
// }
The string & K part is a common pattern because keyof T can include symbols, which can’t be used in template literals. This intersection ensures we’re only working with the string keys. This is your go-to tool for generating API client types or formalizing naming conventions.
Filtering Properties by Value Type
You can use the as clause for another brilliant trick: filtering out properties. By using a conditional type that evaluates to never, you can effectively remove a key from the resulting type.
// Remove all properties that are not functions
type Methods<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
class Example {
name: string = "";
getName() { return this.name; }
calculate() { return 42; }
}
type ExampleMethods = Methods<Example>;
// Equivalent to { getName: () => string; calculate: () => number; }
// The 'name' property is filtered out.
This is incredibly useful for picking apart large, complex types to isolate just the bits you need, like all the methods of a class or all the string fields of an interface.
Pitfalls and Best Practices
The biggest “gotcha” is that mapped types work only on object types with known keys. If you try to use them on a primitive like string, keyof string gives you a union of its method names ("toString" | "charAt" | ...), which is almost never what you want. Always check your input.
Also, remember that these transformations happen at the type level, during compilation. They have zero runtime cost, which is fantastic, but also means you can’t dynamically decide which keys to map based on runtime values. That logic must be encoded in the type system using conditional types and the as clause, as shown above.
Finally, while it’s tempting to get fancy, often the built-in utilities like Partial, Pick, and Omit are exactly what you need. Reach for custom mapped types when you have a specific, repetitive transformation pattern that isn’t covered by the standard library. Don’t reinvent the wheel unless you’re building a better car.