Conditional Types

A type that resolves to one of two different types, chosen based on a check performed entirely at compile time.

What is it?

Normal code can branch based on a runtime condition — an if statement picks one path or another depending on a value known while the program is running. Types can branch too, just at a different time: while the compiler is checking your code, before anything runs.

A conditional type looks like a ternary expression, but written with types instead of values: T extends U ? X : Y. It reads as: "if type T is assignable to type U, resolve to type X; otherwise, resolve to type Y." This lets a type alias produce a different result depending on what type it's given — for example, a type that resolves to true or false depending on whether the input is a string.

Conditional types become especially powerful combined with infer, which lets you extract and name a piece of a type inside the extends check, instead of just testing it. infer is how TypeScript can express things like "give me the return type of this function" or "give me the type inside this array," purely as a type-level computation.

Explain like I'm 10

A conditional type is like a sorting machine on an assembly line: each item that comes down the belt (a type) gets tested against a gauge, and depending on whether it fits, it's routed onto one of two different output belts — all decided automatically, before the item ever reaches the end of the line, based purely on its shape.

Examples

A basic conditional type

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

// Conditional types are often used to build safer utility types:
type NonNullableCustom<T> = T extends null | undefined ? never : T;

type C = NonNullableCustom<string | null>; // string

IsString<T> checks, purely at the type level, whether T is assignable to string, and resolves to the literal type true or false accordingly. NonNullableCustom uses the same mechanism to strip null/undefined out of a type.

Extracting a type with infer

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>;          // number (unchanged — not a Promise)

// TypeScript's own built-in ReturnType<T> works the same way:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: "Ana" };
}

type User = MyReturnType<typeof getUser>; // { id: number; name: string }

infer U introduces a new type variable, U, that TypeScript fills in by matching the shape of Promise<infer U> against the actual type — effectively pulling out whatever was inside the Promise. The same trick, applied to a function's return position, is how the built-in ReturnType<T> is implemented.

How it works

When the compiler evaluates T extends U ? X : Y, it checks — purely structurally, at compile time — whether every value of type T would also be a valid value of type U. If so, the whole expression resolves to X; if not, it resolves to Y. When infer appears inside the extends clause, the compiler doesn't just check a yes/no match — it tries to match the overall shape (like Promise<...> or a function signature) against T, and whatever type lines up with the infer placeholder gets bound to that new type variable, which then becomes available to use in the X branch. If T is itself a union, the conditional type is checked against each member of the union separately and the results are combined back into a union — this behavior is called a distributive conditional type.

Why does it exist?

Some type-level logic genuinely needs to branch on what kind of type it's looking at — extracting the awaited value out of a Promise type, pulling a function's return type out of its signature, or filtering a union down to just the members that match a pattern. Conditional types (with infer) give the type system a way to express that branching and extraction directly, without which those transformations would be impossible to describe generically.

When to use it

Reach for a conditional type when you're building a reusable, generic type transformation whose result genuinely depends on the shape of its input — extracting a piece of a wrapped type, building type-level utilities beyond what's built in, or filtering a union type down based on some structural test.

When not to use it

For everyday application code, conditional types are rarely necessary — they mostly show up inside library and utility-type code. If a simple union, mapped type, or one of the built-in utility types already expresses what you need, prefer that; conditional types (especially with infer) are noticeably harder for other developers to read at a glance.

Common mistakes

  • Writing a conditional type expecting it to run once, without realizing that when T is a union, the condition is checked separately against each member (distributive conditional types), which can produce a broader union than expected.

  • Using infer outside of an extends clause, where it isn't valid — infer can only appear as part of a conditional type's structural check.

  • Overusing conditional types for logic that would be clearer and easier to read as a plain union or a simpler mapped type.

Practice exercises

  1. Easy:

    Write a conditional type IsArray<T> that resolves to true if T is an array type, and false otherwise.

  2. Medium:

    Write a conditional type ElementType<T> that, given an array type T, uses infer to extract and return the type of its elements (e.g. ElementType<string[]> is string).

  3. Hard:

    Write a conditional type Flatten<T> that, given either T or T[], always resolves to T — flattening away one level of array wrapping if present, and leaving non-array types unchanged.

Interview questions

What is a conditional type?

A type-level expression of the form T extends U ? X : Y that resolves to X if T is assignable to U, and to Y otherwise, evaluated entirely at compile time.

What does the `infer` keyword do inside a conditional type?

It introduces a new type variable that the compiler fills in by structurally matching the surrounding pattern (like Promise<infer U>) against the actual type, letting you extract a piece of that type for use in the result.

What is a distributive conditional type?

The behavior where, if the type being checked in a conditional type is a union, the condition is applied separately to each member of the union and the results are combined back into a union, rather than being checked once against the whole union.

Does a conditional type always distribute whenever T is a union, or is there a requirement for that to happen?

Distribution only happens when the type being tested is a naked type parameter — referenced directly, with nothing wrapped around it — in the extends position. Wrapping it, e.g. [T] extends [U] ? X : Y, suppresses distribution, so the union is checked as a whole in a single pass instead of member-by-member.

How do you deliberately prevent a conditional type from distributing over a union?

Wrap both sides of extends in a one-tuple: [T] extends [U] ? X : Y. Wrapping T in [T] makes it something other than a naked type parameter, so the compiler checks the whole union against U at once instead of distributing member-by-member.

Given `type ToArray<T> = T extends any ? T[] : never;`, what does `ToArray<string | number>` resolve to?

string[] | number[] — because T is naked, the conditional distributes: it's evaluated separately as string extends any ? string[] : never (giving string[]) and number extends any ? number[] : never (giving number[]), and the two results are unioned back together.

Given `type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;`, what does `ToArrayNonDist<string | number>` resolve to, and how does that differ from the distributive version?

(string | number)[] — wrapping in [T] extends [any] suppresses distribution, so the whole union string | number is checked and transformed as one unit, producing a single array type of the union rather than a union of two separate array types.

What happens when `infer` sits in a position where multiple union members could each match a different candidate, e.g. inferring the return type from `(() => string) | (() => number)`?

TypeScript infers a union of all the candidate types it finds — here string | number — because when the constraint appears in a covariant (output) position, multiple matches combine with a union.

How does inference differ when `infer` appears in a contravariant position (a function parameter) versus a covariant position (a return type)?

In a covariant position, multiple candidates combine into a union. In a contravariant position, they instead combine into an intersection — because narrowing a function's parameter type safely, so it still accepts everything the wider type accepted, requires satisfying every candidate at once, not just one of them.

Write the conditional type that reimplements the built-in `Parameters<T>`.

type MyParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; — infer P captures the entire parameter list as a tuple type, matched against the ...args rest position.

Why can't a simple, non-recursive conditional type like `T extends Promise<infer U> ? U : T` fully unwrap `Promise<Promise<Promise<string>>>` down to `string`?

It only checks and unwraps one layer per evaluation — the inferred U is returned exactly as matched, without re-running the same check against it. Fully unwrapping nested Promises needs recursion: type Await<T> = T extends Promise<infer U> ? Await<U> : T;, re-applying itself to whatever U was inferred as, until T is no longer a Promise.

What does the recursive `type Await<T> = T extends Promise<infer U> ? Await<U> : T;` resolve to for `Await<Promise<Promise<string>>>`, and how does the recursion terminate?

string — each evaluation strips one layer of Promise and recurses on what's left (Promise<string>, then string); recursion stops once T no longer extends Promise<infer U>, so the conditional falls to the : T branch and returns the final non-Promise type.

What limit does TypeScript impose on recursive conditional types, and what happens if you exceed it?

The compiler caps how deep a conditional type can recurse; exceeding it produces a compile error ("Type instantiation is excessively deep and possibly infinite") rather than looping forever, since it can't always tell a genuinely deep-but-finite recursion apart from one that truly never terminates.

How is a conditional type with `infer` different from a function overload for expressing "the return type depends on the input"?

An overload lists a fixed, finite number of concrete input/output signature pairs, resolved at the call site based on which one matches. A conditional type instead expresses the relationship generically, as one reusable type-level rule that works for any input satisfying the pattern — including inputs the author never explicitly enumerated — rather than a fixed list of cases.

How would you write a conditional type `Flatten<T>` that turns `string[]` into `string` but leaves plain `string` unchanged?

type Flatten<T> = T extends (infer U)[] ? U : T; — if T matches the shape "array of something," infer U captures that element type and the result is U; otherwise the : T branch returns T unchanged.

`type ElementType<T> = T extends Array<infer U> ? U : never;` applied as `ElementType<string | number[]>` produces `number`, not something involving `string`. Is that a bug?

No — it's correct distributive behavior. Because T is naked, the conditional runs separately per union member: string extends Array<infer U> ? U : never fails to match and resolves to never, while number[] extends Array<infer U> ? U : never matches and resolves to number. The results union as never | number, and since never disappears from a union, only number is visible.

Why does `type IsNever<T> = [T] extends [never] ? true : false;` need the tuple-wrapping trick instead of `T extends never ? true : false`?

never is the "empty union" — a conditional type distributing over a naked never type parameter doesn't run its branches at all; it resolves straight to never itself before any comparison happens. Wrapping in [T] extends [never] prevents distribution, so the check actually runs and can correctly return true or false.

How could a conditional type detect whether a given type is specifically `any`?

type IsAny<T> = 0 extends 1 & T ? true : false; exploits the fact that any is the one type where 1 & T collapses back to any (making 0 extends any true); for every other type, 1 & T becomes either 1 or never, and 0 doesn't extend either, so it resolves to false.

How is checking `T extends unknown` different from checking `T extends any` in a conditional type?

T extends unknown is always true for every T (everything is assignable to unknown) and, being a naked check, still distributes normally over unions — often used as a distribution-triggering no-op. T extends any is a genuinely special case, since any disables most normal type-system rules, so conditional checks against it can behave inconsistently and are generally avoided as a real type-level test.

Write a conditional type `NonFunctionKeys<T>` that resolves to a union of just the property names of T whose values are not functions.

type NonFunctionKeys<T> = { [K in keyof T]: T[K] extends (...args: any) => any ? never : K }[keyof T]; — the inner mapped type turns function-valued keys into never and keeps the rest as themselves, and indexing the whole mapped type with [keyof T] collects the surviving values into one union.

Why do conditional types need to exist — couldn't the same transformations be written as separate, non-generic type aliases for each case?

Because the point is reusability across arbitrary, not-yet-known input types — a conditional type expresses "if whatever type you give me has this shape, produce this related type" once, generically, so it keeps working for user-defined types the author never saw. Hardcoding separate aliases per case would mean rewriting the logic for every new type that comes along.

`type Includes<T, U> = T extends U ? true : false;` called as `Includes<string | number, string>` produces `boolean` instead of the expected `true`. Why?

Distribution: because T is naked, the check runs separately per union member — string extends string is true, number extends string is false — and the two results union into true | false, which TypeScript displays as boolean. Testing "does the whole union match" requires suppressing distribution with [T] extends [U] ? true : false instead.

Is a conditional type computed once globally, or re-evaluated per use?

The compiler evaluates (instantiates) a conditional type separately for each distinct combination of type arguments it's actually used with — it isn't computed once globally. Repeated instantiations with identical arguments are typically cached internally for performance, but semantically each concrete substitution is evaluated on its own.