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 parameter —
T extends ...distributes;{ x: T } extends ...does not. neveris the empty union —never extends X ? A : Bisnever(vacuous distribution).inferbinding 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 objectguards 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
- Generics primitives this builds on: 01_generics.md
- Reimplementing built-in utility types: 04_utility_types_from_scratch.md
- Template literal types in key remapping: 03_template_literal_types.md