Dataslope logoDataslope

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 T produces a union of T's property names (we'll explore keyof deeply in keyof, typeof & Template Literals).
  • K in keyof T iterates over each key.
  • T[K] is an indexed access type: the type of property K in T.

Here's a simple example that copies a type unchanged:

Code Block
TypeScript 5.7

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 readonly to make it immutable.
  • Suffix a property with ? to make it optional.

Here's a type that makes every property readonly:

Code Block
TypeScript 5.7

And here's one that makes every property optional:

Code Block
TypeScript 5.7

Removing Modifiers with -

You can also remove modifiers with -:

  • -readonly removes the readonly modifier.
  • -? removes the optional modifier (making it required).
Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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 : Y

If T is assignable to U, the type resolves to X; otherwise, Y.

Here's a simple example:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

Distributive behavior is useful for filtering unions. For example, extracting non-nullable types:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

Here's how infer works:

  1. T extends (...args: any[]) => infer R checks if T is a function.
  2. If it is, R is inferred to be the return type.
  3. If not, the type is never.

You can use infer in multiple positions:

Code Block
TypeScript 5.7

infer is powerful for unwrapping nested types. For example, extracting the element type of an array:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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>

Challenge
TypeScript 5.7
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

QuestionSelect one

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

QuestionSelect one

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 readonly and optional modifiers with +/-.
  • Remap keys with as to rename or filter them.
  • Write conditional types with T extends U ? X : Y.
  • Leverage distributive behavior to process unions member-by-member.
  • Use infer to 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.

On this page