Conditional types power ReturnType, Awaited and most advanced library typings; you need them to read and write those.
A conditional type picks one of two types based on assignability:
type IsString<T> = T extends string ? true : false;It's a type-level ternary. Its power comes from two features.
Distribution. When the checked type is a naked type parameter and you instantiate it with a union, the condition is applied to each member separately and the results are unioned. That's how Exclude works:
type Exclude<T, U> = T extends U ? never : T;
// Exclude<"a" | "b" | "c", "a"> → "b" | "c"Usually that's what you want, but not always. Wrap both sides in a tuple — [T] extends [U] ? ... — to treat the union as a whole. Also note that never is the empty union, so a distributive conditional over never yields never — a common source of puzzling results.
`infer`. Inside the extends clause you can declare a type variable to capture part of the matched type:
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Unwrap<T> = T extends Promise<infer V> ? V : T;This is how ReturnType, Parameters and Awaited are defined. You can constrain an inferred variable directly — infer N extends number — and match several pieces at once, such as the first and rest of a tuple: T extends [infer Head, ...infer Tail].
Conditional types stay deferred while they depend on an unresolved type parameter: inside a generic function, the compiler can't decide T extends string ? A : B until T is known, so returning a value of type A there usually needs an assertion or an overload. That's a known limitation; keep complex conditional types at API boundaries and simple code inside.
Example
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<"hi" | 42>; // boolean (distributed: true | false)
type C = [string | number] extends [string] ? 1 : 0; // 0 (not distributed)
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Unwrap<T> = T extends Promise<infer V> ? Unwrap<V> : T;
type FirstArg<F> = F extends (first: infer P, ...rest: never[]) => unknown ? P : never;
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : undefined;
type PortNumber<S> = S extends `${infer N extends number}` ? N : never;
type E = ElementOf<string[]>; // string
type U = Unwrap<Promise<Promise<number>>>; // number
type F = FirstArg<(id: number, name: string) => void>; // number
type H = Head<[boolean, string]>; // boolean
type P = PortNumber<"8080">; // 8080
const checks: [A, B, C, E, U, F, H, P] = [true, false, 0, "x", 1, 2, true, 8080];
console.log(checks);Remember
T extends U ? X : Yis a type-level if/else- Naked type parameters distribute over unions;
[T] extends [U]prevents it infer Xcaptures part of a matched type- Conditional types on unresolved generics stay deferred
Watch out for
- Being surprised that
IsString<never>isnever, notfalse— distribution over the empty union yields nothing.