Conditional Types & the infer Keyword

0/5 in this phase0/21 across the roadmap

📖 Concept

Conditional types are TypeScript's if/else at the type level: T extends U ? TrueType : FalseType.

They let you create types that choose different outputs based on input types — the foundation of advanced type manipulation.

The infer keyword declares a type variable INSIDE a conditional type that TypeScript infers for you. It's like a capture group in regex — it extracts a type from a pattern.

Key patterns with infer:

  • T extends Promise<infer R> ? R : T — Unwrap a Promise
  • T extends (...args: infer P) => infer R ? R : never — Extract return type
  • T extends [infer First, ...infer Rest] ? First : never — Extract array first element

Distributive conditional types: When a conditional type is applied to a union, it distributes — applying the condition to EACH member of the union separately. T extends U ? X : Y applied to A | B becomes (A extends U ? X : Y) | (B extends U ? X : Y).

This distribution is what makes Exclude<T, U> work: Exclude<'a' | 'b' | 'c', 'a'> distributes and removes 'a'.

🏠 Real-world analogy: Conditional types are like a sorting machine. Items go in, and based on their properties, they exit through different chutes. infer is like an X-ray scanner inside the machine that reads what's inside a package without opening it.

💻 Code Example

codeTap to expand ⛶
1// Basic conditional type
2type IsString<T> = T extends string ? "yes" : "no";
3type A = IsString<string>; // "yes"
4type B = IsString<number>; // "no"
5
6// Practical: unwrap a Promise
7type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
8type Result1 = UnwrapPromise<Promise<string>>; // string
9type Result2 = UnwrapPromise<number>; // number (not a Promise)
10
11// Deeply unwrap nested Promises
12type DeepUnwrap<T> = T extends Promise<infer R> ? DeepUnwrap<R> : T;
13type Deep = DeepUnwrap<Promise<Promise<Promise<number>>>>; // number
14
15// Extract return type (how ReturnType works internally)
16type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
17type FnReturn = MyReturnType<(x: number) => string>; // string
18
19// Extract function parameters (how Parameters works internally)
20type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
21type FnParams = MyParameters<(a: string, b: number) => void>;
22// [a: string, b: number]
23
24// Extract array element type
25type ElementOf<T> = T extends (infer E)[] ? E : never;
26type Item = ElementOf<string[]>; // string
27type Items = ElementOf<(string | number)[]>; // string | number
28
29// Distributive conditional types
30type ToArray<T> = T extends any ? T[] : never;
31type Distributed = ToArray<string | number>;
32// string[] | number[] (NOT (string | number)[])
33
34// Non-distributive: wrap in tuple to prevent distribution
35type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
36type NonDist = ToArrayNonDist<string | number>;
37// (string | number)[]
38
39// Practical: extract event handler payload
40type ExtractPayload<T> = T extends { type: string; payload: infer P } ? P : never;
41
42type ClickEvent = { type: "click"; payload: { x: number; y: number } };
43type LoginEvent = { type: "login"; payload: { userId: string } };
44
45type ClickPayload = ExtractPayload<ClickEvent>; // { x: number; y: number }
46type LoginPayload = ExtractPayload<LoginEvent>; // { userId: string }
47
48// infer with constraints (TS 4.7+)
49type FirstString<T> = T extends [infer S extends string, ...any[]] ? S : never;
50type FS = FirstString<["hello", 42]>; // "hello"
51// type FN = FirstString<[42, "hi"]>; // never (first isn't string)

🏋️ Practice Exercise

Mini Exercise:

  1. Write UnwrapArray<T> that extracts the element type from an array type
  2. Create IsNever<T> that returns true if T is never, false otherwise (tricky!)
  3. Build ExtractPromiseChain<T> that deeply unwraps nested Promises
  4. Write FunctionReturnType<T> that works like the built-in ReturnType
  5. Create Head<T> and Tail<T> types for tuple types using infer

⚠️ Common Mistakes

  • Not understanding distributive behavior — T extends any ? T[] : never distributes over unions, which might not be what you want

  • Forgetting to wrap in tuple [T] to prevent distribution — [T] extends [U] prevents the union from being split

  • Using infer outside of conditional types — infer ONLY works in the extends clause of a conditional type

  • Making conditional types too deeply nested — if you need 5+ levels, consider breaking into smaller named types

  • Not realizing that never distributes to neverConditional<never> often returns never unexpectedly because never is the empty union

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Conditional Types & the infer Keyword