Conditional Types & the infer Keyword
📖 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 PromiseT extends (...args: infer P) => infer R ? R : never— Extract return typeT 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
1// Basic conditional type2type IsString<T> = T extends string ? "yes" : "no";3type A = IsString<string>; // "yes"4type B = IsString<number>; // "no"56// Practical: unwrap a Promise7type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;8type Result1 = UnwrapPromise<Promise<string>>; // string9type Result2 = UnwrapPromise<number>; // number (not a Promise)1011// Deeply unwrap nested Promises12type DeepUnwrap<T> = T extends Promise<infer R> ? DeepUnwrap<R> : T;13type Deep = DeepUnwrap<Promise<Promise<Promise<number>>>>; // number1415// Extract return type (how ReturnType works internally)16type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;17type FnReturn = MyReturnType<(x: number) => string>; // string1819// 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]2324// Extract array element type25type ElementOf<T> = T extends (infer E)[] ? E : never;26type Item = ElementOf<string[]>; // string27type Items = ElementOf<(string | number)[]>; // string | number2829// Distributive conditional types30type ToArray<T> = T extends any ? T[] : never;31type Distributed = ToArray<string | number>;32// string[] | number[] (NOT (string | number)[])3334// Non-distributive: wrap in tuple to prevent distribution35type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;36type NonDist = ToArrayNonDist<string | number>;37// (string | number)[]3839// Practical: extract event handler payload40type ExtractPayload<T> = T extends { type: string; payload: infer P } ? P : never;4142type ClickEvent = { type: "click"; payload: { x: number; y: number } };43type LoginEvent = { type: "login"; payload: { userId: string } };4445type ClickPayload = ExtractPayload<ClickEvent>; // { x: number; y: number }46type LoginPayload = ExtractPayload<LoginEvent>; // { userId: string }4748// 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:
- Write
UnwrapArray<T>that extracts the element type from an array type - Create
IsNever<T>that returnstrueif T isnever,falseotherwise (tricky!) - Build
ExtractPromiseChain<T>that deeply unwraps nested Promises - Write
FunctionReturnType<T>that works like the built-inReturnType - Create
Head<T>andTail<T>types for tuple types usinginfer
⚠️ Common Mistakes
Not understanding distributive behavior —
T extends any ? T[] : neverdistributes over unions, which might not be what you wantForgetting to wrap in tuple
[T]to prevent distribution —[T] extends [U]prevents the union from being splitUsing
inferoutside of conditional types —inferONLY works in theextendsclause of a conditional typeMaking conditional types too deeply nested — if you need 5+ levels, consider breaking into smaller named types
Not realizing that
neverdistributes tonever—Conditional<never>often returnsneverunexpectedly becauseneveris the empty union
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Conditional Types & the infer Keyword