Conditional Types and infer
Introduction
Conditional types pick one of two types based on a condition, similar to a ternary for types. The infer keyword extracts types inside that condition—powering utilities like ReturnType and Awaited. You will mostly use these patterns through built-ins; this chapter teaches enough to read library typings and write small helpers.
Prerequisites
Conditional Type Syntax
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;
type B = IsString<number>;Code explanation:
T extends string ? true : falseevaluates at type levelAistrue,Bisfalse
Read as: “If T is assignable to string, result is true, else false.”
Distributive Behavior on Unions
When T is a naked type parameter, conditional types distribute over unions:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[]Code explanation:
- Distribution applies to
string | numberseparately - Wrap with tuple
[T]to disable distribution when you need a single result
Practical: Exclude Revisited
Conceptually, Exclude works like:
type MyExclude<T, U> = T extends U ? never : T;T extends U ? never : T removes union members assignable to U.
infer: Extract Types from Structure
type ElementType<T> = T extends (infer E)[] ? E : never;
type Item = ElementType<string[]>;
// stringCode explanation:
infer Edeclares a type variable matched in theextendspattern- For
string[],Ebecomesstring
Function return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "Ada" };
}
type User = MyReturnType<typeof getUser>;infer R captures whatever the function returns.
Promise unwrap (simplified Awaited)
type MyAwaited<T> = T extends Promise<infer U> ? U : T;
type Value = MyAwaited<Promise<number>>;
// numberReal Awaited handles nested promises; this shows the idea.
infer in Function Parameters
type FirstArg<T> = T extends (first: infer F, ...rest: never[]) => unknown ? F : never;
type A = FirstArg<(name: string, age: number) => void>;
// stringUseful for typing wrappers around callbacks.
Nested Conditionals
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: "object";
type S = TypeName<"x">;
type N = TypeName<42>;Prefer utility types or helper aliases when chains grow long.
Template Literal Types (Brief)
Conditionals combine with string literal types:
type EventName = "click" | "focus";
type HandlerName<T extends EventName> = `on${Capitalize<T>}`;
type ClickHandler = HandlerName<"click">;
// "onClick"Common in DOM and CSS typings; advanced styling topic.
When to Write Your Own
| Situation | Recommendation |
|---|---|
Need Partial / Pick | Use built-in utilities |
| Library wrapping unknown functions | Consider infer + conditional |
| App domain models | Prefer interfaces and unions |
| Deep recursive types | Proceed carefully—compiler performance |
Warning
Complexity Budget
If a conditional type needs a paragraph to explain, split helpers or simplify the API shape.
Example: Unwrap Array or Single
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type A = Flatten<string[]>;
type B = Flatten<number>;A is string, B is number.
FAQ
Do conditional types run at runtime?
No. They exist only during type checking.
Why does my conditional type become never?
Often incompatible extends branches or distribution over empty unions. Simplify T and test each branch.
infer outside conditional types?
infer is only valid in the extends clause of conditional types.
Difference from union narrowing?
Narrowing is value-level control flow. Conditionals transform types at compile time.
Should beginners write conditional types daily?
No. Recognize them in definitions; use built-ins first.
What is T extends any?
Legacy pattern; prefer unknown in modern strict code when matching anything.