AniUI Academy

Conditional Types

Types that branch on a condition with extends ? :, how they distribute automatically over unions, and extracting a nested type with infer.

10 min read

Everything covered so far describes a fixed shape. A conditional type lets a type itself branch, based on a check against another type — the type-level equivalent of an if/else, evaluated entirely at compile time.

The basic form

type IsString<T> = T extends string ? true : false;
 
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

extends here doesn't mean class inheritance — it means "is T assignable to string?", the exact same meaning extends had as a generic constraint earlier in this track. The conditional type reads left to right: if the check holds, resolve to the first branch; otherwise, the second.

A more useful example

type NonNullable<T> = T extends null | undefined ? never : T;
 
type A = NonNullable<string | null>; // string
type B = NonNullable<number>;        // number

This is, in fact, extremely close to how the built-in NonNullable<T> from the utility types lessons is actually implemented — a conditional type that resolves to never (removing that case) whenever T is null or undefined.

Distributive conditional types

Something specific and worth understanding deliberately happens when T in T extends U ? X : Y is a naked type parameter and you apply the conditional to a union: TypeScript checks each member of the union separately, then unions the results back together, rather than checking the whole union as one combined type:

type ToArray<T> = T extends unknown ? T[] : never;
 
type Result = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Which becomes:   string[] | number[]

This is why NonNullable<string | null> above correctly strips out just the null case and keeps string, rather than the whole union collapsing to never the moment any member fails the check — each member is judged on its own.

infer: capturing part of a type

infer introduces a new type variable inside a conditional type's condition, letting you pull a piece out of a matched structure without already knowing what it is:

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
 
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>;          // number — T didn't match Promise<...>, so T itself

Read it as: "if T is some Promise wrapping something, capture that something as U and resolve to it; otherwise, just resolve to T unchanged." This is genuinely how Awaited<T>, covered in the utility types lessons, is implemented (with extra handling for nested promises and thenables) — and it's the same mechanism behind ReturnType<T>:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 
function getUser() {
  return { name: "Amara" };
}
 
type User = MyReturnType<typeof getUser>; // { name: string }

infer R here captures whatever the function's return type actually is, using the general shape (...args: any[]) => infer R as the pattern to match against.

Where this shows up in practice

You will very rarely need to write a conditional type with infer in everyday application code — but recognising the pattern matters, because it's exactly how a large portion of the utility types you already use (ReturnType, Parameters, Awaited, NonNullable) are actually defined under the hood. Understanding conditional types is what turns those from "magic built-ins" into "ordinary type-level code you could have written yourself."

Try it yourself

Try it yourself
Loading playground...

What to remember

  • A conditional type (T extends U ? X : Y) branches at compile time based on whether T is assignable to U.
  • When T is a bare type parameter and the check is applied to a union, the conditional distributes over each member separately before unioning the results.
  • infer captures part of a matched type's structure into a new type variable, usable in the true branch.
  • Several familiar utility types (ReturnType, Awaited, NonNullable) are themselves ordinary conditional types using infer.

Check yourself

4 questions · pass 3/4 to unlock Mapped Types

up to 50
  1. 1.What does type IsString<T> = T extends string ? true : false; evaluate to for IsString<"hi">?

  2. 2.Given a conditional type T extends U ? X : Y, what happens when T is a union like A | B?

  3. 3.What does infer do inside a conditional type, e.g. T extends Promise<infer U> ? U : never?

  4. 4.What does ReturnType<T> (from the utility types lessons) actually use internally to extract a function's return type?

4 left to answer