frontend / typescript / 02_conditional_and_mapped_types.md

Conditional and Mapped Types (and infer)

5 min read source

Conditional and Mapped Types (and infer)

TL;DR

Conditional types, mapped types, and infer together form TypeScript’s expression layer — the part of the type system that computes new types from existing ones. Every utility type (Partial, Pick, ReturnType, etc.) is built from these three primitives. Senior-level knowledge: when conditionals distribute over unions (and how to disable it with [T]), how infer extracts types from positions, and how as in a mapped type lets you remap or filter keys.

Interview Q&A

Q: What’s a conditional type?

T extends U ? A : B — at the type level. Resolved structurally.

type IsString<T> = T extends string ? true : false

type A = IsString<'hello'>  // true
type B = IsString<42>       // false

Conditionals get powerful when combined with generics and infer.

Q: What’s a distributive conditional type?

When the checked type is a naked generic parameter, the conditional distributes over a union — it’s applied to each member separately and the result unioned.

type ToArray<T> = T extends any ? T[] : never

type R = ToArray<string | number>
// = (string extends any ? string[] : never) | (number extends any ? number[] : never)
// = string[] | number[]      ← distributed
// NOT (string | number)[]

Useful for “map each member of a union.” Sometimes unwanted — see next.

Q: How do you disable distribution?

Wrap the checked type in a tuple [T]:

type ToArrayNoDist<T> = [T] extends [any] ? T[] : never

type R = ToArrayNoDist<string | number>
// = (string | number)[]    ← NOT distributed

This is how built-in Exclude would behave differently if it didn’t distribute (which is exactly why Exclude does distribute).

Q: What does infer do?

infer X introduces a fresh type variable inside a conditional type and binds it to whatever shape matches. It’s the type-level equivalent of pattern matching.

type ReturnT<F> = F extends (...args: any[]) => infer R ? R : never

type A = ReturnT<() => number>                 // number
type B = ReturnT<(x: string) => boolean>       // boolean
type C = ReturnT<string>                       // never

You can infer from arrays, promises, tuples, function parameters — any structural position.

type ElementOf<T> = T extends (infer E)[] ? E : never
type Awaited1<T>  = T extends Promise<infer V> ? V : T
type FirstParam<F> = F extends (a: infer A, ...rest: any[]) => any ? A : never

Q: What’s a mapped type?

A mapped type iterates over a key set and produces a new object type.

type Stringify<T> = { [K in keyof T]: string }

interface User { id: number; name: string; active: boolean }
type S = Stringify<User>
// = { id: string; name: string; active: string }

The K in keyof T syntax is the iteration.

Q: What modifiers can you apply in a mapped type?

readonly and ? (optional), each prefixable with + or - to add or remove:

type MyPartial<T>   = { [K in keyof T]?: T[K] }                  // adds ?
type MyReadonly<T>  = { readonly [K in keyof T]: T[K] }          // adds readonly
type MyMutable<T>   = { -readonly [K in keyof T]: T[K] }         // removes readonly
type MyRequired<T>  = { [K in keyof T]-?: T[K] }                 // removes ?

-? is how Required is implemented.

Q: What’s key remapping with as?

A mapped type can rewrite its key with as NewKey. Combined with template literal types, this lets you transform property names:

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

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

And as never filters a key out — used to build Omit-style types:

type StripFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
}

Q: Build DeepReadonly<T>.

type DeepReadonly<T> =
  T extends (...args: any[]) => any ? T :
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
  T

Three branches: skip functions (don’t add readonly to a function), recurse into objects, leave primitives alone.

Q: When does distribution bite you?

When you want a conditional about a union rather than over each member. Compare:

// Distributes — checks each union member.
type NonNullable1<T> = T extends null | undefined ? never : T

// Doesn't distribute — checks the whole union.
type IsUnion<T> = [T] extends [infer U] ? U : never  // not actually useful as-is

// Classic "did we get a union" detector — uses distribution intentionally:
type UnionToIntersection<U> =
  (U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never

If T is a naked parameter, it distributes; if wrapped in [T] or another constructor, it doesn’t.

Q: When does a recursive type explode?

Mapped/conditional types that recurse into themselves on deeply nested input can hit the TypeScript instantiation depth limit (~50–100). You’ll see Type instantiation is excessively deep and possibly infinite. The fix is usually to bound recursion or simplify the shape.

// Risky on big inputs:
type DeepKeys<T> = T extends object
  ? { [K in keyof T]: K | DeepKeys<T[K]> }[keyof T]
  : never

Gotchas / edge cases

  • Distribution requires a naked type parameterT extends ... distributes; { x: T } extends ... does not.
  • never is the empty unionnever extends X ? A : B is never (vacuous distribution).
  • infer binding scope — bound only inside that one branch; can’t reuse it outside.
  • Mapped over union of object types{ [K in keyof (A | B)]: ... } uses only the shared keys; usually not what you wanted.
  • Recursive type-instantiation limit — TS won’t dive infinitely; use T extends object guards and bounded depth where possible.
  • { [K in keyof T]: T[K] } is structurally identical to T — but the act of mapping can lose modifiers; reapply them with +readonly/+? if needed.

What a senior is expected to say

A junior calls Partial<T> and Pick<T, K> “TypeScript built-ins.” A senior knows they’re trivial mapped types built from keyof T + [K in ...] + the ? / readonly modifiers — and can reimplement them on the spot. A senior also names distribution as the property that makes Exclude/Extract/NonNullable work, knows how to disable it with [T] extends [U], and uses infer for extracting types from positions (return type, promise value, array element, function parameter).

Cross-references

Further reading