Conditional types and infer: types that make decisions
Write T extends U ? X : Y, understand why conditional types spread over unions and how to stop it, and pull types out of other types with infer.
Comes up in: Coding
Learn#
Nikhil writes a fetchJson<T>() helper, and every caller types Promise<User> by hand, then again User after the await. He wants one type that says “whatever is inside this promise”.
A conditional type chooses a type with the same shape as a ternary: T extends U ? X : Y. If T fits U, the result is X; otherwise Y.
Inside the extends part, infer declares a type variable that TypeScript fills in by matching, so you can pull a piece out of a type:
type IsString<T> = T extends string ? "yes" : "no";
type NoSpread<T> = [T] extends [string] ? "yes" : "no";
type Unwrap<T> = T extends Promise<infer V> ? V : T;
const a: IsString<"hi"> = "no"; // "yes" only
const b: IsString<string | number> = "maybe"; // each member checked
const c: NoSpread<string | number> = "yes"; // the union checked as one
const d: Unwrap<Promise<number>> = "42"; // infer pulled out numberexample.ts(5,7): error TS2322: Type '"no"' is not assignable to type '"yes"'.
example.ts(6,7): error TS2322: Type '"maybe"' is not assignable to type '"yes" | "no"'.
example.ts(7,7): error TS2322: Type '"yes"' is not assignable to type '"no"'.
example.ts(8,7): error TS2322: Type 'string' is not assignable to type 'number'.Line 6 shows a distributive conditional type: given a union, it runs once per member and joins the answers.
State
- T = string | number
IsString<string | number>: T is a union, and T stands alone on the left of extends.
Read the steps as text
- IsString<string | number>: T is a union, and T stands alone on the left of extends.
- So the check runs once per member. string extends string → "yes".
- number extends string → "no".
- The results are joined back into a union: "yes" | "no". Wrapping T in [T] would check the whole union once instead.
Predict#
Predict first
type ToArray<T> = T extends any ? T[] : never. What is ToArray<string | number>?
T stands alone before extends, so the type distributes: string[] for one member, number[] for the other. Writing [T] extends [any] would give (string | number)[]. The first choice is tempting because that is how you would write it by hand.
Interview angle#
Say it like this — about 30 seconds
“A conditional type picks a type based on a check: T extends U ? X : Y. When T is a naked type parameter and you pass a union, it distributes: the check runs for each member and the results are joined. That is how Exclude works. Wrapping both sides in a tuple, like [T] extends [U], turns that off. infer declares a type variable inside the check, so ReturnType is F extends (…args: any) => infer R ? R : never.”
Follow-up Implement ReturnType.
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;Follow-up What does distributive mean?
Follow-up Why is IsNever<never> tricky?
never is an empty union, so a distributive check runs zero times and returns never. Use [T] extends [never].Practice#
Guided
Write four conditional types. The tests are type checks: they pass when TypeScript reports no errors.
Done when
ElementType<string[]>isstring, and a readonly array works too.UnwrapPromise<Promise<Promise<number>>>isnumber.MyReturnTypeworks for any function type.IsNever<never>istrueandIsNever<string>isfalse.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
What is the shape of a conditional type?
T extends U ? X : Y
When does a conditional type distribute over a union?
When the checked type is a naked type parameter.
How do you stop distribution?
Wrap both sides in a tuple: [T] extends [U].
What does infer do?
Declares a type variable inside extends that TypeScript fills in by matching.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Exclude<T, U> is T extends U ? never : T. It distributes, so each member that matches becomes never, and never disappears from a union.
Check yourself: What does Exclude<'a' | 'b', 'a'> give, and why?
'b'. Exclude distributes, turning 'a' into never.
How it works underneathSenior rounds
infer can appear anywhere a type can: [infer Head, ...infer Tail], Promise<infer V>, or a function’s parameters. Since TypeScript 4.7 you can add a constraint, infer V extends string Source: TypeScript Handbook: Conditional types.
Check yourself: Can infer appear several times in one check?
Yes, for example to take both the first and the rest of a tuple.
Edge cases and trapsWhere people slip
If T is still a type parameter, the conditional type stays deferred, so inside a generic function TypeScript cannot narrow it and you may need a cast. any as input gives both branches joined.
Check yourself: What is a generic conditional type before T is known?
Deferred: TypeScript keeps it unresolved until T is known.
In productionStaff-level answers
Built-ins such as Awaited, ReturnType, NonNullable and Parameters are conditional types Source: TypeScript Handbook: Utility types. In app code, use them sparingly and test them like code (TS.06.02).
Check yourself: Where do conditional types show up in libraries you use?
Built-ins such as ReturnType and Awaited, and API types in libraries like React and tRPC.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- What is a conditional type? What does "distributive" mean?
- How does
inferwork? ImplementReturnType.