Notifications

No notifications

/Phase 4

Mapped & Conditional Types

This is where TypeScript's type system becomes a small functional language. Mapped types loop over keys to transform a shape; conditional types branch on a type; infer extracts pieces from another type. Together they power every utility type you've already used.

On this page

Detailed Theory

Mapped types — { [K in keyof T]: … }

A mapped type walks over the keys of T and produces a new shape:

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

That's it — that's all Partial and Readonly are.

Modifiers — + and -

You can add or remove ? and readonly:

type Required<T>  = { [K in keyof T]-?: T[K] };       // strip optional
type Mutable<T>   = { -readonly [K in keyof T]: T[K] }; // strip readonly

Key remapping — as

Rename keys while you map:

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

type User = { id: number; name: string }; type UserGetters = Getters<User>; // { getId: () => number; getName: () => string }

You can also filter keys by remapping to never:

type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

Conditional types — T extends U ? X : Y

Branches at the type level:

type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>;   // false

Distribution over unions

Conditional types distribute over naked union type parameters:

type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>;
// (string 
number)[] ? No → string[]
number[]

That's surprising the first time. To disable distribution, wrap in tuples:

type ToArray2<T> = [T] extends [any] ? T[] : never;
type Y = ToArray2<string 
number>; // (stringnumber)[]

Filtering unions

type NonNull<T> = T extends null
undefined ? never : T; type S = NonNull<string
null
undefined>; // string

That's literally how NonNullable works.

infer — pull a type out

Inside the extends clause you can introduce a new type variable with infer:

type ElementOf<A> = A extends Array<infer U> ? U : never;
type N = ElementOf<number[]>;     // number
type S = ElementOf<string[]>;     // string

type ReturnType2<F> = F extends (...args: any[]) => infer R ? R : never; type R = ReturnType2<() => Date>; // Date

type AwaitedT<T> = T extends Promise<infer U> ? U : T; type X = AwaitedT<Promise<number>>; // number

Real-world: pull params, return types, promise contents, JSX prop types — all powered by infer.

Combining mapped + conditional

The classic "deep readonly":

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

type Config = { db: { host: string }; flags: { debug: boolean } }; type Frozen = DeepReadonly<Config>;

Mutable counterpart:

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

Pick keys whose value extends a type:

type KeysOfType<T, V> = {
  [K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];

type StringKeys = KeysOfType<{ id: number; name: string; email: string }, string>; // "name"

"email"

That {...}[keyof T] trick is the pattern for "give me the keys whose value matches a predicate".

Template literal types

Strings as types you can compose:

type Route = /api/${"users"
"posts"}/${number}
; type R = "/api/users/1"; // assignable to Route

type EventName<T extends string> = on${Capitalize<T>}; type E = EventName<"click">; // "onClick"

Pair with mapped types for component prop generation:

type Listeners<T extends string> = {
  [K in T as on${Capitalize<K>}]: (e: Event) => void;
};
type Btn = Listeners<"click" | "focus">;
// { onClick: (e: Event) => void; onFocus: (e: Event) => void }

When to reach for these

  • ✅ You're building a library or shared types.
  • ✅ You need to derive types from existing ones (DRY at the type level).
  • ❌ You're writing application code with one-off shapes — keep it simple.
Powerful, but readable types win. If a type takes 10 minutes to read, it'll cost you in PRs.