Conditional types let a type depend on another type, exactly as a ternary expression lets a value depend on a condition. They are the engine behind ReturnType, Exclude, Awaited and most library typings that "know" the result of an operation from its inputs. This lesson teaches the syntax, the distributive behaviour over unions that surprises most people, and the infer keyword for pulling types apart.
type Check<T> = T extends string ? "text" : "other";
type A = Check<"hello">; // "text"
type B = Check<42>; // "other"T extends U ? X : Y reads: if T is assignable to U, the result is X, otherwise Y. Branches may themselves be conditional types, giving type-level if/else if chains:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends (...args: any[]) => any ? "function" :
"object";
type N = TypeName<() => void>; // "function"While T is still an unresolved type parameter the conditional is deferred and stays unevaluated until a concrete type is supplied.
When the checked type is a naked type parameter and you pass a union, the conditional is applied to each member separately and the results are unioned. This is called a distributive conditional type:
type ToArray<T> = T extends unknown ? T[] : never;
type R1 = ToArray<string | number>; // string[] | number[] (distributed)Distribution is what makes the built-in filters work:
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Status = "idle" | "loading" | "done" | "error";
type Busy = MyExclude<Status, "idle" | "done">; // "loading" | "error"Each member that matches becomes never, and never vanishes from a union. Two consequences follow:
ToArray<never> is never, because distributing over the empty union yields nothing.[T] extends [unknown] ? T[] : never, which gives (string | number)[] for the example above.infer declares a type variable inside the extends clause. If the match succeeds, that variable holds the corresponding part of the type:
type FirstArg<F> = F extends (first: infer A, ...rest: any[]) => any ? A : never;
type Ret<F> = F extends (...args: any[]) => infer R ? R : never;
type PromiseValue<P> = P extends Promise<infer V> ? V : P;
type TupleHead<T> = T extends [infer H, ...unknown[]] ? H : never;
type A = FirstArg<(id: number, name: string) => void>; // number
type B = Ret<typeof JSON.parse>; // any
type C = PromiseValue<Promise<Blob>>; // Blob
type D = TupleHead<[boolean, string]>; // booleanThe built-in ReturnType<T>, Parameters<T>, ConstructorParameters<T>, InstanceType<T> and Awaited<T> are all defined this way. Since TypeScript 4.7 an inferred variable can carry its own constraint, infer U extends string, which avoids a second conditional check.
A conditional type may reference itself, which unlocks deep transformations:
type DeepAwaited<T> = T extends Promise<infer V> ? DeepAwaited<V> : T;
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type F = Flatten<number[][][]>; // number
type Join<T extends string[], Sep extends string> =
T extends [infer H extends string, ...infer R extends string[]]
? R extends [] ? H : `${H}${Sep}${Join<R, Sep>}`
: "";
type Path = Join<["users", "42", "posts"], "/">; // "users/42/posts"The compiler limits recursion depth (roughly 50 levels for non-tail positions, more for tail-recursive forms), so keep these for bounded structures such as tuples and nested objects.
| Need | Conditional type |
|---|---|
| Different return type per input type | T extends string ? number : string |
| Remove members from a union | Exclude<T, U> |
| Read a function's result type | ReturnType<T> via infer |
| Unwrap wrappers (Promise, arrays) | infer on the wrapper's type argument |
| Compute a literal from a tuple | Recursive conditional with infer |
T extends string ? ... : ... to narrow a value; conditional types operate on types only. Use overloads or guards for runtime behaviour.string[] | number[] when (string | number)[] was intended.any into results; (...args: any[]) => any is fine for matching, but prefer unknown elsewhere.What is `ToArray<string | number>` when `type ToArray<T> = T extends unknown ? T[] : never`?
T extends U ? X : Y selects a type based on assignability and can be chained like if/else.[T] to prevent it.never results vanish from unions, which is how Exclude, Extract and NonNullable filter.infer captures part of a matched type, enabling ReturnType, Parameters, Awaited and custom unwrappers.Next lesson: Mapped Types and Key Remapping — build new object types by iterating over the keys of existing ones.