The introduction of Conditional Types in TypeScript 2.8 marked the evolution of the type system into a Turing-complete functional programming language executed at static compile time. Conditional types enable non-linear relationships between input types and output data structures, selecting one of two evaluation branches based on a subtyping relationship.
The syntax of a conditional type mirrors JavaScript's ternary operator:
type Result = T extends U ? TrueBranch : FalseBranch;If type T is a subtype of type U (T <: U), the TrueBranch is evaluated; otherwise, execution falls through to FalseBranch.
Two foundational mechanisms expand the expressive power of conditional types:
infer Keyword: A pattern-matching mechanism that declares an inline type variable within the extends clause to capture nested structural sub-components.The combination of conditional types and infer underpins all modern type-level metaprogramming architectures: from standard library utilities like ReturnType, Parameters, and Awaited to complex schema parsers for RPC libraries and ORMs.
When evaluating a conditional type, the compiler inspects the nature of operand T. If T is an unadorned naked type parameter and the input supplied is a union A | B | C, the operation expands distributively:
+-------------------------------------------------------------------------+
| DISTRIBUTIVITY OF CONDITIONAL TYPES |
| (A | B) extends U ? TrueBranch : FalseBranch |
+-------------------------------------------------------------------------+
| | |
| Expanded by the compiler into a union of branches: |
| v |
| (A extends U ? TrueBranch : FalseBranch) |
| | (Union) |
| (B extends U ? TrueBranch : FalseBranch) |
+-------------------------------------------------------------------------+
| |
| DISABLING DISTRIBUTIVITY VIA TUPLE WRAPPING [T] |
| [A | B] extends [U] ? TrueBranch : FalseBranch |
| | |
| v |
| Evaluates the union (A | B) monolithically as a single unit |
+-------------------------------------------------------------------------+The infer keyword functions as a pattern extractor: the compiler reconciles the incoming type against the target shape and binds the unknown position to a newly minted type identifier.
infer without manually maintaining duplicated client-side response contracts.Promise<Promise<string[]>> into a flat string[] type when handling network streams.type SafePayload<T> = T extends AppError ? never : T.Payload type from action creators: type ExtractPayload<T> = T extends ActionCreator<infer P> ? P : never.The compiler checks whether T is a naked parameter. If a Union (A | B) is detected, it initiates distributive branching across each member.
Type T is unified against the extends target. If infer R is present, the structural component is captured into candidate variable R.
Multiple candidates for R in output positions yield a Union (|); multiple candidates in input positions yield an Intersection (&).
Substitutes inferred R into TrueBranch or falls back to FalseBranch, performing final type normalization.
never Trap#In TypeScript, never is mathematically an empty union ($0$ members). Consequently, distributive conditional evaluation over never produces counterintuitive behavior if left unwrapped:
// ❌ NAIVE BUGGY IMPLEMENTATION:
export type IsNeverWrong<T> = T extends never ? true : false;
// Why does IsNeverWrong<never> return never instead of true?
// Because never is an EMPTY union, the distributive loop runs 0 times!
// Result: an empty union => never!
type Test1 = IsNeverWrong<never>; // never!
// ✅ CORRECT IMPLEMENTATION: disable distributivity with tuple wrapping [T]
export type IsNever<T> = [T] extends [never] ? true : false;
type Test2 = IsNever<never>; // true!
type Test3 = IsNever<string>; // false!
type Test4 = IsNever<unknown>; // false!Disabling distributivity is equally essential when designing a type-level predicate to detect whether a type is a union:
// Check if a type is a union:
// Trick: [T] disables distributivity in the outer comparison,
// while naked T in the inner expression distributes across each member!
export type IsUnion<T, U = T> = T extends any
? [U] extends [T]
? false
: true
: never;
type Case1 = IsUnion<string>; // false
type Case2 = IsUnion<string | number>; // true
type Case3 = IsUnion<"a" | "b" | "c">; // true
type Case4 = IsUnion<[string, number]>; // false (tuple is monolithic)infer: Extracting Parameters and Returns#The infer keyword enables pattern-matched destructuring of function contracts:
// 1. Extract the first argument of a function
export type FirstArgument<TFunction> = TFunction extends (
first: infer TFirst,
...rest: readonly unknown[]
) => unknown
? TFirst
: never;
// 2. Extract the last element of a tuple or array
export type LastElement<TTuple extends readonly unknown[]> = TTuple extends readonly [
...unknown[],
infer TLast
]
? TLast
: never;
// 3. Extract the resolved value of an asynchronous function
export type AsyncReturnType<TFunction extends (...args: readonly unknown[]) => unknown> =
TFunction extends (...args: readonly unknown[]) => Promise<infer TResult>
? TResult
: ReturnType<TFunction>;
// Demonstration
async function fetchUserMetrics(userId: string) {
return { userId, score: 98.5, active: true };
}
type TargetFirst = FirstArgument<typeof fetchUserMetrics>; // string
type TargetReturn = AsyncReturnType<typeof fetchUserMetrics>; // { userId: string, score: number, active: boolean }
type TargetTuple = LastElement<[number, string, boolean]>; // booleanAwaited<T>#Starting in TypeScript 4.5, Awaited<T> is built as a deeply recursive conditional type, unwrapping nested PromiseLike chains of arbitrary depth with built-in cycle detection:
// Architectural implementation of a recursive Thenable unwrapper
export type DeepAwaited<T> = T extends null | undefined
? T // If null/undefined is passed, return as-is
: T extends object & { then(onfulfilled: infer F, ...args: readonly unknown[]): unknown }
? F extends (value: infer V, ...args: readonly unknown[]) => unknown
? DeepAwaited<V> // Recursively unwrap the resolved value
: never
: T; // Base scalar type if T is not a promise
// Demonstration of deep unwrapping
type Level0 = DeepAwaited<string>; // string
type Level1 = DeepAwaited<Promise<string>>; // string
type Level2 = DeepAwaited<Promise<Promise<number>>>; // number
type Level3 = DeepAwaited<Promise<Promise<Promise<boolean>>>>; // booleaninfer Behavior Across Multiple Occurrences#When the same type variable infer R appears in multiple positions inside a pattern, the compiler applies variance rules:
|).&).// Contravariant position: function parameters merge into an Intersection (&)
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type DisjointUnion = { role: "admin" } | { permissions: string[] };
// Type-level synthesis: the union is transformed into an intersection!
type Intersected = UnionToIntersection<DisjointUnion>;
// Result: { role: "admin" } & { permissions: string[] }Below is a production metaprogramming utility that introspects any function signature (synchronous or asynchronous):
export type FunctionDescriptor<TFunction> = TFunction extends (
this: infer TThis,
...args: infer TArgs
) => infer TReturn
? {
thisContext: TThis;
parameters: TArgs;
parameterCount: TArgs extends { length: infer L } ? L : 0;
rawReturnType: TReturn;
isAsync: TReturn extends Promise<unknown> ? true : false;
resolvedReturnType: TReturn extends Promise<infer TResolved>
? TResolved
: TReturn;
}
: never;
// Usage demonstration
class PaymentService {
async processTransaction(this: PaymentService, orderId: string, amount: number) {
return { transactionId: "tx-42", status: "COMPLETED" as const };
}
}
export type PaymentMethodInfo = FunctionDescriptor<PaymentService["processTransaction"]>;
/*
PaymentMethodInfo resolves at compile time to:
{
thisContext: PaymentService;
parameters: [orderId: string, amount: number];
parameterCount: 2;
rawReturnType: Promise<{ transactionId: string; status: "COMPLETED" }>;
isAsync: true;
resolvedReturnType: { transactionId: string; status: "COMPLETED" };
}
*/Because never represents an empty union, T extends never distributively executes 0 times and unconditionally returns never. All checks for never must be wrapped in tuples: [T] extends [never].
// ❌ BROKEN: returns never when passed never
type CheckNever<T> = T extends never ? "is-never" : "not-never";
type Result1 = CheckNever<never>; // never!
// ✅ FIXED: non-distributive check via tuples
type CheckNeverSafe<T> = [T] extends [never] ? "is-never" : "not-never";
type Result2 = CheckNeverSafe<never>; // "is-never"If a conditional type wraps an unshielded parameter in an array, distributivity produces a union of arrays (A[] | B[]) rather than an array of unions ((A | B)[]), breaking collections handling.
// ❌ BUG: transforms string | number into string[] | number[]
type ToArrayDistributive<T> = T extends unknown ? T[] : never;
type MixedList1 = ToArrayDistributive<string | number>; // string[] | number[]
// ✅ FIXED: tuple isolation preserves union cohesion
type ToArrayMonolithic<T> = [T] extends [unknown] ? T[] : never;
type MixedList2 = ToArrayMonolithic<string | number>; // (string | number)[]The infer keyword is permitted exclusively within the extends clause of a conditional type. Attempting to use infer inside interface bodies, generic argument lists, or naked return positions triggers syntax error TS1338.
// ❌ SYNTAX ERROR TS1338: 'infer' declarations are only permitted in the 'extends' clause
type BadExtract<T> = infer R;
// ✅ CORRECT: infer used inside the extends condition
type GoodExtract<T> = T extends infer R ? R : never;In naive recursive infer implementations targeting functions without return values, void may accidentally collapse into undefined or never, violating event handler contracts.
The compiler inspects type T: if a union is detected and T is an unwrapped naked parameter, distributive branching is enabled.
The conditional expression is duplicated for each individual constituent member of the union.
For each branch, the type structure is unified against the pattern, binding unknown type variables.
All evaluated branches are combined back into a normalized result union.