Mapped & Conditional Types
Transform and filter types with mapped types, conditional logic, and the infer keyword.
You've learned how to build generic types that work with any input. But what if you need to transform a type systematically — taking every property and making it optional, or readonly, or changing its value type? What if you need conditional logic at the type level — "if this type extends that, then produce X, otherwise Y"?
These questions lead us to mapped types and conditional types, two of TypeScript's most powerful metaprogramming tools. They let you write type-level functions that inspect, filter, and reshape other types. They're the building blocks behind the standard library's utility types (which we'll tour in the next chapter), and they're essential for modeling complex, real-world constraints.
Mapped Types
A mapped type iterates over the keys of a type and produces a new type by transforming each property. The syntax looks like this:
type Mapped<T> = {
[K in keyof T]: T[K];
};Let's unpack:
keyof Tproduces a union ofT's property names (we'll explorekeyofdeeply in keyof, typeof & Template Literals).K in keyof Titerates over each key.T[K]is an indexed access type: the type of propertyKinT.
Here's a simple example that copies a type unchanged:
This CopiedUser is structurally identical to User. But mapped types become powerful when you change something during the mapping.
Modifiers: readonly and ?
Inside a mapped type, you can add or remove modifiers:
- Prefix a property with
readonlyto make it immutable. - Suffix a property with
?to make it optional.
Here's a type that makes every property readonly:
And here's one that makes every property optional:
Removing Modifiers with -
You can also remove modifiers with -:
-readonlyremoves thereadonlymodifier.-?removes the optional modifier (making it required).
The + prefix is implicit (the default), so +readonly is the same as readonly. But being explicit helps when you're toggling modifiers dynamically.
Key Remapping with as
TypeScript 4.1 introduced key remapping in mapped types: you can use as to rename keys during the mapping. The new key must resolve to a string | number | symbol (or never to exclude that key).
Here's a type that prefixes every key with get:
We'll cover template literal types like `get${...}` in the next chapter. For now, notice that K is remapped to a new string literal, and the property type becomes a function returning T[K].
You can also filter keys by remapping to never:
The condition T[K] extends string ? K : never keeps only keys whose values are strings. Keys that don't match are mapped to never, which TypeScript omits.
Conditional Types
A conditional type selects one of two types based on whether a constraint is satisfied. The syntax mirrors a ternary operator:
T extends U ? X : YIf T is assignable to U, the type resolves to X; otherwise, Y.
Here's a simple example:
Conditional types become powerful when combined with generics. You can build type-level logic that branches based on the input.
Distributive Conditional Types
When a conditional type is applied to a naked type parameter (one not wrapped in an array, tuple, or other construct), TypeScript distributes the conditional over each member of a union:
Notice that Result is string[] | number[], not (string | number)[]. Each member of the union was processed separately.
To prevent distribution, wrap the type parameter in brackets:
Distributive behavior is useful for filtering unions. For example, extracting non-nullable types:
The infer Keyword
The infer keyword lets you extract parts of a type inside a conditional. It's like pattern matching for types.
For example, here's how to extract the return type of a function:
Here's how infer works:
T extends (...args: any[]) => infer Rchecks ifTis a function.- If it is,
Ris inferred to be the return type. - If not, the type is
never.
You can use infer in multiple positions:
infer is powerful for unwrapping nested types. For example, extracting the element type of an array:
How Mapped and Conditional Types Relate
Mapped types transform object types by iterating over keys. Conditional types branch based on type relationships. Together, they let you build sophisticated transformations: "map over this type's keys, and for each key, if its value is X, do Y; otherwise, do Z."
Putting It Together
Let's build a type that makes all function properties in an object optional, leaving other properties unchanged:
Here, the conditional type checks if T[K] is a function. If so, it unions with undefined. Otherwise, it's unchanged.
Challenge: Implement Mutable<T>
The standard library provides Readonly<T>, which adds readonly to every property. Your task is to implement the opposite: Mutable<T>, which removes readonly from every property.
Hint: Use a mapped type with the -readonly modifier.
Multiple Choice: Distributive Behavior
Given:
type Wrap<T> = T extends any ? { value: T } : never;
type Result = Wrap<"a" | "b">;
What is Result?
{ value: "a" | "b" }
{ value: "a" } | { value: "b" }
never
Multiple Choice: Key Remapping
Given:
type Test<T> = { [K in keyof T as K extends string ? K : never]: T[K] };
type Result = Test<{ a: number; 0: string }>;
What properties does Result have?
Both a and 0
Only a
Neither (empty object)
Summary
You've learned how to:
- Map over a type's keys with
{ [K in keyof T]: ... }. - Add or remove
readonlyand optional modifiers with+/-. - Remap keys with
asto rename or filter them. - Write conditional types with
T extends U ? X : Y. - Leverage distributive behavior to process unions member-by-member.
- Use
inferto extract parts of a type.
These are the foundational tools for advanced type modeling. In the next chapter, we'll tour the standard library's utility types — and you'll see that they're all built from mapped and conditional types. There's no magic; you now have the tools to build them yourself.
Next: Utility Types — a practical tour of Partial, Pick, ReturnType, and friends, plus how they're implemented.