Engrain by ByteRush

TS.05.02 Mid-level· 11 minCore — asked often

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 number
TypeScript compiler output (verified)
example.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.

How a conditional type spreads over a unionStep 1 of 4

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
  1. IsString<string | number>: T is a union, and T stands alone on the left of extends.
  2. So the check runs once per member. string extends string → "yes".
  3. number extends string → "no".
  4. 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>?

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.”

Stays on your device. Nothing is uploaded.
Follow-up Implement ReturnType.
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
Follow-up What does distributive mean?
A conditional type on a naked type parameter is applied to each union member separately, and the results are joined into a new union.
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[]> is string, and a readonly array works too.
  • UnwrapPromise<Promise<Promise<number>>> is number.
  • MyReturnType works for any function type.
  • IsNever<never> is true and IsNever<string> is false.
Your solution
Ctrl/⌘ + Enter

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 infer work? Implement ReturnType.