Conditional Types & infer
A conditional type chooses between two types based on a test, using the same ? : syntax as a JavaScript ternary. Combined with the infer keyword, it can pull smaller types out of bigger ones — which is how ReturnType, Awaited and many library types work.
The syntax
A extends B ? X : Y means "if A is assignable to B, the result is X, otherwise Y":
type IsString<T> = T extends string ? "yes" : "no"; type A = IsString<"hello">; type B = IsString<42>; const a: A = "yes"; const b: B = "no"; console.log(a, b);
yes no
Conditional types are almost always generic — the test only makes sense when T isn't known yet.
A useful example: different outputs for different inputs
type IdFor<T> = T extends { kind: "user" } ? `user_${number}` : `item_${number}`; function makeId<T extends { kind: "user" | "item" }>(entity: T, n: number): IdFor<T> { return `${entity.kind}_${n}` as IdFor<T>; } const uid = makeId({ kind: "user" }, 7); const iid = makeId({ kind: "item" }, 3); console.log(uid, iid);
user_7 item_3
The caller gets a precise return type — uid is typed `user_${number}`. Notice the as IdFor<T> inside: TypeScript can't check a function body against an unresolved conditional type, so implementations usually need an assertion. That's a sign to use conditional types sparingly in your own functions.
Distribution over unions
When the checked type is a bare type parameter and you pass a union, the conditional is applied to each member separately and the results are combined:
type ToArray<T> = T extends unknown ? T[] : never; type Result = ToArray<string | number>; const x: Result = ["a", "b"]; const y: Result = [1, 2]; console.log(x, y);
[ 'a', 'b' ] [ 1, 2 ]
Result is string[] | number[], not (string | number)[]. This distributive behaviour is how Exclude is built:
type MyExclude<T, U> = T extends U ? never : T; type MyNonNullable<T> = T extends null | undefined ? never : T; type Colors = MyExclude<"red" | "green" | "blue", "green">; const c: Colors = "blue"; const n: MyNonNullable<string | null> = "ok"; console.log(c, n);
blue ok
Each member that matches becomes never, and never disappears from a union. To turn distribution off, wrap both sides in brackets: [T] extends [U] ? … : ….
Extracting types with infer
Inside the extends clause, infer X declares a placeholder that TypeScript fills in by matching. If the match succeeds, you can use X in the true branch:
type ElementOf<T> = T extends (infer E)[] ? E : never; type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never; type Unpromise<T> = T extends Promise<infer V> ? V : T; type A = ElementOf<string[]>; type B = MyReturnType<() => boolean>; type C = Unpromise<Promise<number>>; const a: A = "text"; const b: B = true; const c: C = 3; console.log(a, b, c);
text true 3
Read T extends (infer E)[] ? E : never as: "if T is an array of something, call that something E and return it". MyReturnType is essentially how the built-in ReturnType is written.
Recursive conditional types
A conditional type can refer to itself, which handles nested structures:
type DeepUnwrap<T> = T extends Promise<infer V> ? DeepUnwrap<V> : T; type Flatten<T> = T extends readonly (infer E)[] ? Flatten<E> : T; type A = DeepUnwrap<Promise<Promise<string>>>; type B = Flatten<number[][][]>; const a: A = "deep"; const b: B = 1; console.log(a, b);
deep 1
Combining with mapped types: DeepReadonly
type DeepReadonly<T> = T extends (...args: any[]) => any ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T; interface AppState { user: { name: string; prefs: { theme: string } }; } const state: DeepReadonly<AppState> = { user: { name: "Jo", prefs: { theme: "dark" } } }; state.user.prefs.theme = "light";
error TS2540: Cannot assign to 'theme' because it is a read-only property.
The built-in Readonly only affects the top level. This version recurses into nested objects, while leaving functions alone.
When to use them
Conditional types are the most advanced tool in this course. They're essential in library code — they power the precise types you get from tools like Prisma, tRPC and Zod — but in application code, a simpler union, overload or generic is usually easier for teammates to read. Reach for them when you need to derive one type from another and nothing simpler works.