skip to content

Conditional Types

Type-level if/else: `T extends U ? X : Y` chooses a type by testing assignability. This is where TypeScript stops being annotations and becomes a small programming language, and interviewers treat it as the senior-level marker.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

14

In TypeScript, what does the type `type IsString<T> = T extends string ? 'yes' : 'no'` do, and what does `IsString<'abc'>` resolve to?

level: juniorimportance: must knowfreq 62%

answer

  1. ternary, but in the type system
  2. left side is the checked type
  3. the test is assignability
  4. resolves when instantiated
  5. erased from the emitted JavaScript

basics

~20 s

A conditional type chooses between two types by testing assignability: if T is assignable to string the type becomes 'yes', otherwise 'no'. IsString<'abc'> resolves to 'yes', because the literal type 'abc' is assignable to string.

solid answer

~40 s

`T extends string ? 'yes' : 'no'` is a conditional type — a ternary that runs in the type system rather than at run time. The test is assignability: the compiler asks whether the type on the left of `extends` can be used where the type on the right is expected. `IsString<'abc'>` is `'yes'`, because the string literal type `'abc'` is assignable to `string`; `IsString<number>` is `'no'`. Both branches are types, not values, and the whole expression is evaluated when the alias is instantiated with a concrete type argument. Like every other type construct it is erased — nothing about the conditional survives into the emitted JavaScript, and no check happens while the program runs.

code

typescript · 8 lines
typescript
type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<'abc'>;   // 'yes' — a literal type is assignable to string
type B = IsString<string>;  // 'yes'
type C = IsString<number>;  // 'no'

// The result is an ordinary type and can be used as one.
const answer: IsString<'abc'> = 'yes';

go deeper

for a junior

Be able to read T extends string ? A : B aloud as a type-level ternary, say the test is assignability, and give the answer for a literal argument like 'abc'. Also say plainly that nothing runs at run time.

for a middle

Explain that the conditional resolves when the alias is instantiated with a concrete argument, that the checked type sits on the left of extends, and that the two branches are independent types rather than values.

for a senior

Show where a conditional type buys real precision in an API and where it mainly costs readable error messages, and be ready to explain what callers see when the conditional cannot yet resolve.

for a principal

Own the judgment about exposing conditionals in a shared types package: weigh the precision they add against the diagnostics your consumers will read when a type fails to match.

## The shape A conditional type has exactly one form: ```ts T extends U ? X : Y ``` Read it as a ternary that lives in the type system. `T` is the *checked* type, `U` is the type it is checked against, and `X` and `Y` are the two possible results. All four positions hold types — you cannot put a value, a runtime expression, or a `typeof x` on a variable there. Everything happens while the compiler is running; the emitted JavaScript contains no trace of it. The usual way to meet one is inside a generic type alias, because that is what makes the construct useful: ```ts type IsString<T> = T extends string ? 'yes' : 'no'; ``` `IsString` is now a type-level function: give it a type argument, get a type back. ## The test is assignability The most common misreading is treating `extends` here as the class keyword — "is T a subclass of string". It is not. In a conditional type, `A extends B` asks a single question: **is a value of type A usable everywhere a value of type B is expected?** That is the ordinary assignability relation the checker uses everywhere else, applied at the type level. So the direction matters. The *more specific* type goes on the left: ```ts type A = IsString<'abc'>; // 'yes' — the literal type 'abc' is assignable to string type B = IsString<string>; // 'yes' — a type is assignable to itself type C = IsString<number>; // 'no' ``` `'abc'` is a *literal type*: the type whose only inhabitant is that one string. Every string literal type is assignable to `string`, which is why `IsString<'abc'>` takes the true branch. ## When it evaluates A conditional type is resolved when the compiler knows what the checked type actually is. Writing the alias declares the rule; writing `IsString<'abc'>` *instantiates* it, and at that point the checker performs the assignability test and replaces the whole conditional with one branch. Hover over `A` in an editor and you see `'yes'` — the conditional has already collapsed. This is why the construct composes: the result is an ordinary type and can be used anywhere a type can be used. ```ts const answer: IsString<'abc'> = 'yes'; // const wrong: IsString<'abc'> = 'no'; // Error: 'no' is not assignable to 'yes' ``` When the checked type is still an unresolved type parameter — inside a generic function body, for instance — the compiler cannot pick a branch yet and keeps the conditional around unevaluated. That deferred state has consequences of its own, and it is worth knowing that it exists. ## Both branches are types A frequent beginner slip is expecting the branches to be values, or expecting them to be the same kind of thing. They need not match at all: ```ts type Result<T> = T extends string ? string[] : never; type Flag<T> = T extends string ? true : false; ``` `true` and `false` here are the boolean *literal types*, not runtime booleans. `never` is the type with no values, and it is a common choice for a branch meaning "this case does not apply". ## Nothing is emitted This follows from the rule that organises all of TypeScript: types are erased. Compile a file whose only content is a conditional type alias and the output JavaScript is empty. There is no runtime cost, no reflection, no way to ask at run time which branch was taken, and no way to make the decision depend on a runtime value. If you need a decision at run time, you write ordinary JavaScript — the conditional type only describes what the compiler should believe. ## Where it earns its place Conditional types are how a type describes a rule instead of a fixed shape: "the return type depends on what you passed in", "this property only exists when that flag is set". Used sparingly they make an API's types honest. Used heavily they make error messages long and hard to read, because the compiler prints the unresolved conditional rather than a friendly name. ## Common mistakes to avoid - Reading `T extends string` as a declaration that constrains `T`. In the conditional position it is a *question*, not a restriction; a conditional type accepts whatever it is given and answers accordingly. - Expecting a runtime check. Nothing is checked at run time. - Writing the wide type on the left and the narrow one on the right, then being surprised the test fails.

  • Does any part of a conditional type survive into the emitted JavaScript?
    No. Types are erased, so a file containing only conditional type aliases compiles to nothing. There is no runtime branch, no helper function and no cost. The conditional only tells the compiler which type to believe, so it can never depend on a runtime value.
  • What does `IsString<any>` give you, and why is that surprising?
    It gives `'yes' | 'no'`. When the checked type is `any`, the compiler cannot commit to either outcome and resolves the conditional to the union of both branches. It is a special rule worth remembering, because a stray `any` flowing into a type-level helper quietly widens every result downstream.
  • Do the two branches have to be related types?
    Not at all. `T extends string ? string[] : never` is perfectly ordinary. The branches are independent types, and using `never` for the false branch is the usual way to say "this case does not apply". The only rule is that both positions hold types, never values.

saying these in an interview costs you the question

  • Says extends here means class inheritance
  • Expects a runtime check in the emitted code
  • Thinks both branches must be the same type
  • Reads it as declaring a constraint on T
  • Believes the branch can depend on a runtime value

context

open as a page

In TypeScript, given `type ToArray<T> = T extends unknown ? T[] : never`, why does `ToArray<string | number>` evaluate to `string[] | number[]` rather than `(string | number)[]`?

level: middleimportance: must knowfreq 62%

basics

~20 s

A conditional type whose checked type is a bare type parameter distributes: TypeScript applies it to each union member separately and unions the results. ToArray runs once on string and once on number, producing string[] | number[].

open as a page

In a TypeScript conditional type, `extends` is often described as "assignable to" rather than "inherits from". What does `type R = { id: number; name: string } extends { id: number } ? 'yes' : 'no'` resolve to, and why?

level: middleimportance: must knowfreq 58%

basics

~20 s

The result is 'yes'. In a conditional type, extends asks whether the left type is assignable to the right one, and an object type with an extra property is assignable to one that requires fewer. No declared inheritance is involved.

open as a page

In a TypeScript conditional type, what does the `infer` keyword do, where may the variable it declares be used, and how would you write an `ElementType<T>` that pulls the element type out of an array type?

level: middleimportance: must knowfreq 62%

basics

~20 s

infer declares a type variable inside the extends clause of a conditional type, letting the compiler pattern-match a shape and capture whatever sits in that slot. The captured variable is in scope only in the true branch.

open as a page

In TypeScript, `type IsString<T> = T extends string ? true : false` gives `boolean` for `IsString<string | number>`. Explain that result and how you would make the check apply to the whole union instead.

level: middleimportance: should knowfreq 50%

basics

~20 s

The conditional distributes over the union: string yields true, number yields false, and true | false is exactly how boolean is defined. Wrapping both sides in one-element tuples, [T] extends [string], disables distribution and returns false.

open as a page

How do you build a type-level switch out of nested TypeScript conditional types, and why does the order of the branches change the result?

level: middleimportance: should knowfreq 44%

basics

~20 s

Chain conditionals in the false branch so the first test that passes wins. Put narrow tests before broad ones: because extends is an assignability test, a broad branch placed first swallows inputs that a later, narrower branch was meant to match.

open as a page

In TypeScript, how do you use `infer` inside tuple patterns to extract a tuple type's first element type, its remaining elements, and its last element type?

level: middleimportance: should knowfreq 40%

basics

~10 s

Match the tuple against a tuple pattern containing infer: [infer H, ...unknown[]] captures the head, [unknown, ...infer R] captures the tail as a tuple, and [...unknown[], infer L] captures the last element.

open as a page

In TypeScript, a helper `type Stripped<T> = Omit<T, 'id'>` applied to a discriminated union collapses it into a single object type and loses the members' own properties. Why does that happen, and how do you keep the union?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Omit is not distributive: T is used inside keyof and Pick, never as a conditional's naked checked type, and keyof of a union yields only the shared keys. Wrap it yourself — T extends unknown ? Omit<T, K> : never — to apply it per member.

open as a page

In TypeScript, why does `type Check<T> = T extends string ? 'yes' : 'no'` evaluate `Check<never>` to `never`, and how do you write a type that reports whether T is `never`?

level: seniorimportance: should knowfreq 34%

basics

~20 s

never is the empty union, and a distributive conditional maps itself over each union member. With zero members there is nothing to map, so the result is the empty union again: never. Detect it non-distributively with [T] extends [never].

open as a page

In TypeScript, `function pick<T extends boolean>(flag: T): T extends true ? string : number { return flag ? 'yes' : 0; }` is rejected at the return statement. Why does the compiler refuse it, and how would you type this API instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

While T is an unresolved type parameter the conditional return type stays deferred, so the compiler cannot prove any returned value matches whichever branch will win. Fix it with overload signatures, or with an assertion inside a loosely typed implementation.

open as a page

In TypeScript, when the same `infer U` name appears at two positions of one conditional type's `extends` clause, when do you get a union of the candidates and when do you get an intersection?

level: seniorimportance: should knowfreq 26%

basics

~10 s

Multiple candidates for one inference variable are combined by position: covariant positions such as property or return types produce a union of the candidates, while contravariant positions such as function parameters produce an intersection.

open as a page

In a TypeScript conditional type, what does adding a constraint to an inference variable — as in `infer N extends number` — change compared with a bare `infer N`?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

A constrained inference variable must satisfy its constraint or the conditional takes the false branch, and in a template-literal placeholder the constraint also makes the compiler infer a literal of that type instead of a string.

open as a page

You maintain a shared TypeScript types package. Whether an exported generic type distributes over unions is invisible in its signature — how do you decide which behaviour a type should have, and how do you keep changing it from silently breaking consumers?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

Decide by intent: per-member transforms should distribute, whole-type judgments should not. Then pin the choice with type-level tests that include union, never and any inputs, encode it in the name, and treat flipping it as a breaking release.

open as a page

When designing a TypeScript library's public types, when should a type be derived from an implementation value using `infer`-based extraction, and when should it be declared explicitly?

level: principalimportance: nice to knowfreq 15%

basics

~20 s

Derive when the implementation value is the real source of truth and drift is the risk. Declare explicitly when the type is a contract others depend on, so an implementation edit cannot silently change what consumers see.

open as a page