Command Palette

Search for a command to run...

Hectal
PHASE 9Advanced ~28 min· topic 1 of 5

Topic 9.1

Conditional Types

In one line

A conditional type, written T extends U ? X : Y, asks whether a type fits another type and picks one of two answers, so a type can change depending on what you give it.

Think of it like this

A post office sorting machine. A parcel comes in, the machine asks one question ("is it lighter than 1 kg?"), and sends it down the left belt or the right belt. A conditional type is a sorting machine for types. You put a type in, it asks "does this type fit that shape?", and it hands back one type for yes and another for no. Nothing is run; the compiler sorts types while it checks your code.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Conditional type
A type that picks one of two types depending on a test, written T extends U ? X : Y.
extends (in a condition)
"Is assignable to": can a value of the left type be used where the right type is expected.
Generic type alias
A named type with a blank (a type parameter) that callers fill in, like type Box<T> = { value: T }.
Type parameter
The blank in a generic, usually written T. The caller replaces it with a real type.
Branch
One of the two answers of a conditional type: the X after ? or the Y after :.
Deferred
Put on hold. While the compiler does not yet know T, it cannot choose a branch, so it keeps the whole conditional type as it is.
Type-level
Happening in the types only, while the compiler checks. It leaves no trace in the JavaScript that runs.
Type assertion (`as`)
Also called a cast (Topic 5.6). You tell the compiler "trust me, this value has this type". It checks nothing at runtime.

Step by step

01Ask a question about a type

Start with the smallest useful conditional type. It asks "is T a string?" and answers with the type true or the type false.

Types can be values too: true and false are literal types (Topic 4.2) that contain exactly one value each. That is why a type can "answer" with them.

is-string.tswhole filets
type IsString<T> = T extends string ? true : false;

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

02Fill in the blank by hand

The compiler replaces T with what you wrote and tests the question. For IsString<42> it asks "does 42 fit in string?" No, so the result is the false branch.

Hover over A and B in the first live example to see true and false. You can try this with any type, including string itself: a value of type string fits in string, so the answer is true.

03Return something more useful than true or false

Answers can be any type. Flatten<T> says: "if T is an array, give me the type of its items, otherwise give me T itself."

T[number] means "the type you get when you index the array type with a number", which is the type of one item. You met this in Topic 8.3 (indexed access types).

flatten.tswhole filets
type Flatten<T> = T extends unknown[] ? T[number] : T;

type Nums = Flatten<number[]>; // number
type Plain = Flatten<boolean>; // boolean

04Chain the questions

Put a second conditional type in the "no" branch to get an if / else if / else. The compiler tries the questions from the top and stops at the first yes.

Always end with a plain fallback. Without it you would have nothing to answer when no question matches.

Chain the questionsdiagram
Rendering diagram…

05Use a condition to shape objects

The branches can be whole object types. Result<T> gives a failure shape for error types and a success shape for everything else. Callers of a function returning Result<T> can then tell the two apart with the ok field (Topic 4.5, discriminated unions).

06The catch inside functions

When T is still a blank (inside a generic function), the compiler cannot pick a branch. It refuses any return whose type is not the whole deferred conditional type, with TS2322.

The fix: name the conditional type, return an honest as cast inside the body, and let callers get the exact answer. The cast is the price of asking the compiler to decide at a time when it cannot know.

shout.tswhole filets
type IdOf<T extends string | number> = T extends string ? string : number;

function shout<T extends string | number>(x: T): IdOf<T> {
  return (typeof x === "string" ? x.toUpperCase() : Number(x) * 2) as IdOf<T>;
}

const a = shout("abc"); // string
const b = shout(21);    // number

Try it yourself

  1. 1

    Add a case to Describe

    In the second example, add a branch so that Describe<undefined> gives "nothing". Predict where to put it (before or after the boolean question?) and check with a new // ^? query.

    It can go anywhere before the final fallback, because undefined does not fit number, string or boolean.

  2. 2

    Peel two layers with Flatten

    In the first example, add Flatten<string[][]> and Flatten<Flatten<string[][]>> with a // ^? each. Predict both before you check.

    The first gives string[] and the second string: one use of T[number] peels one array layer.

  3. 3

    Break the cast

    In the third example, change as IdOf<T> to as string. A new TS2322 error appears on the return line: a plain string is still not the deferred conditional type. The lines const a and const b still say string and number, because callers only read the declared return type IdOf<T>.

    So the cast has to name the conditional type itself. That is the one place where you promise the compiler something it cannot check.

Code & diagrams

Ask a yes/no question about a type, then reshape it

Read each answer in the Types panel. Then change IsString<42> to IsString<"a" | 42> and predict the result before you look (Topic 9.2 explains it).

Console

(nothing was printed)
Chained questions and object-shaped answers

RangeError fits the shape of Error, so it takes the first branch. Try Result<string> and Result<Error | string> and read what you get.

Console

(nothing was printed)
A conditional return type needs one honest cast

Callers get exact types (string for "abc", number for 21). Inside wrong(), TypeScript will not trust a plain string or number, because it cannot yet tell which branch T will take. The code still runs, since the types are erased.

Console

(nothing was printed)

Expected console output

ABC 42

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Return a plain value for a conditional return type

In the third example, look at wrong: it declares a conditional return type and returns a string literal and a number with no cast.

terminal
$ Types panel
── what you'll see ──
index.ts(14,34): error TS2322: Type 'string' is not assignable to type 'T extends string ? string : number'.

Myth vs fact

Myth

extends in a conditional type means the same as extends in a class.

Fact

No. Here it asks "is the left type assignable to the right type?". A literal type like "hello" extends string, and { a: 1; b: 2 } extends { a: number }. Nothing is inherited.

Myth

A conditional type runs when the program runs.

Fact

No. It is computed by the compiler while it checks your code and leaves nothing in the JavaScript output. It cannot read a variable's value, only a type.

Myth

TypeScript will check my function body against each branch separately.

Fact

No. With a generic T it can't know the branch, so it checks against the unresolved conditional type, which no ordinary value fits. That is why a cast or an overload is needed.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The check T extends U uses assignability, not identity, so { a: 1; b: 2 } extends { a: number }. To test for "exactly the same type" you need a trick with two generic functions, and even that has edge cases (any, intersections). Topic 10.6 shows it when we test types.

  • ▸

    A conditional type with an unresolved T is stored as a deferred conditional type object. It resolves the moment T becomes concrete, which is why shout("abc") returns string while wrong's body sees the raw conditional. If T is any, the result is the union of both branches: type Yn<T> = T extends string ? "yes" : "no" gives "no" | "yes" for Yn<any>. That is a special rule worth remembering when you meet any in a generic.

  • ▸

    Conditional types are cached per set of type arguments. They are cheap one at a time, but deeply nested ones in a generic-heavy codebase add up in the type-check time. When a build slows down, an unhelpfully clever conditional type is a common suspect (Topic 10.6).

Remember this

  1. 1

    The problem this solves: sometimes the type of a result depends on the type of the input. A function that gives back a string when you give it a string and a number when you give it a number is an example. With only the tools from earlier phases you would have to write the answer as string | number and lose the link between input and output, or repeat the same type by hand for each case. A conditional type keeps that link, in one place.

  2. 2

    The syntax is T extends U ? X : Y. Read it as an ordinary question: "does T fit inside U? If yes, the result is X, otherwise it is Y." The word extends here does not mean inheritance. It means is assignable to, the same test the checker uses when you assign one value to a variable of another type (Topic 3.5). So "hello" extends string is yes, and 42 extends string is no.

  3. 3

    A conditional type almost always lives inside a generic type alias (a type with a type parameter, like type IsString<T> = ...). The parameter T is a blank the caller fills in. IsString<"hello"> fills the blank with "hello", the compiler asks the question, and the answer is true. You never call it like a function: you write it where a type is expected and the compiler computes it.

  4. 4

    You can chain conditions: put another conditional type in the "no" branch, like if, else if, else. Always end the chain with a plain fallback type. The compiler checks branches in order and takes the first one that fits, so put the narrow cases before the wide ones.

  5. 5

    Conditional types are best at reshaping a type based on its shape. Flatten<T> turns number[] into number and leaves boolean alone. Result<T> can give an error shape for Error types and a success shape for everything else. This is how the built-in utilities work: you will see in Topic 10.2 that NonNullable, Exclude and ReturnType are only a few lines each.

  6. 6

    Generic functions are where it gets tricky. Inside a function body, T is still an unknown blank, so the compiler cannot decide which branch applies. Such a conditional type is deferred (put on hold). If you annotate a function's return type as T extends string ? string : number, TypeScript rejects every return statement in the body, because no concrete value is known to match a branch that has not been picked yet. The usual fix is one honest as cast on the return, with a name for the conditional type so callers still get the exact answer.

  7. 7

    A conditional type is a type-level decision, so it cannot see runtime values. It decides from types only. When you need a different result for different argument types, an overload (Topic 2.5) is often simpler and is checked properly inside the body. Reach for a conditional return type when the output type is a clean function of the input type and you have more than a couple of cases.

Explain it without notes

01

What does T extends U ? X : Y mean, and what is extends testing?

02

How does a conditional type differ from an if statement?

03

Why does TypeScript reject return "text" in a function whose return type is T extends string ? string : number?

04

When would you choose a conditional return type, and when an overload?

Practice

01

Write type Nullable<T> = ... that is true when null fits in T and false otherwise. Test it on string, string | null and null.

02

Write type Wrap<T> = ... that gives T[] when T is not already an array and T itself when it is. Test it on number and number[].

03

Write a generic function double<T extends string | number>(x: T) whose return type is string for strings (the text repeated twice) and number for numbers (doubled). Use a named conditional type and one cast.

Trade-offs

  • ↔

    A conditional return type gives callers exact types with a single signature. The price is a cast in the body that the compiler cannot check, so a wrong branch there is a bug nobody catches. Overloads trade a few extra lines for a body that is checked.

  • ↔

    Chained conditional types are compact but hard to read. Name each helper and add a hover example (// ^?) in a comment so the next reader can see the answer.

Done when you can

  • I can read T extends U ? X : Y aloud as an if / else on types.

  • I can explain that extends here means "is assignable to", not inheritance.

  • I can build a chained conditional type with a plain fallback.

  • I can use // ^? to check what a conditional type resolves to.

  • I can explain why a generic function needs a cast for a conditional return type.