cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Conditional types

Write types that choose with T extends U ? X : Y, use them with type parameters, and see how unions are split one member at a time, and how to stop it.

By the end, you can
  • 01
    Read and write a conditional typeRead T extends U ? X : Y as a question, "does T fit U?", and pick the answer it gives.
  • 02
    Use it with a type parameterWrite types like Flatten<T> and FieldValue<Kind> whose answer depends on the type passed in.
  • 03
    Control how unions are checkedPredict how a union is split one member at a time, filter it with never, and stop the split with [T].

A type that picks one of two answers

In JavaScript, the conditional operator ? : picks one of two values (the The rest of the operators lesson covers it):

A label that picks one of two wordsPop out in the code editor (opens in a new tab)JavaScript
const quantity = 1;
const label = quantity === 1 ? "item" : "items";
console.log(`${quantity} ${label}`);

Line 1 sets quantity to 1. Line 2 asks a question, quantity === 1. If the answer is yes, the label is "item"; if no, it's "items". Line 3 prints 1 item.

TypeScript has the same ? : for types. A conditional type asks a question about a type and picks one of two types as the answer. Here a form field's kind decides what kind of value it holds:

A field's value type, picked from its kindPop out in the code editor (opens in a new tab)TypeScript
type FieldValue<Kind> = Kind extends "checkbox" ? boolean : string; type Subscribe = FieldValue<"checkbox">;//   ^?type Email = FieldValue<"email">;//   ^?

Line 1 has four parts:

  • FieldValue<Kind> is a type with a type parameter, Kind: a name for a type that you fill in each time you use it, like an argument to a function (the Generic types & interfaces lesson covers them).
  • Kind extends "checkbox" is the question: "does Kind fit "checkbox"?"
  • ? boolean is the answer for yes. A checkbox is ticked or not, so its value is true or false.
  • : string is the answer for no. Every other kind of field holds text.

Line 3 fills in Kind with "checkbox", and the query on line 4 shows the yes answer, boolean. Line 5 uses "email", and the query on line 6 shows the no answer, string. Now pick a kind yourself: type it on line 3 and watch the answer on line 4.

Playground: pick a field kind
Ask FieldValue about one kindPop out in the code editor (opens in a new tab)TypeScript
type FieldValue<Kind> = Kind extends "checkbox" ? boolean : string; type Looked = FieldValue<"checkbox">;//   ^?
The kind"checkbox"
What TypeScript says
Lookedboolean
ErrorsNo errors
Try it yourself

"checkbox" fits "checkbox", so the answer is the yes branch: boolean. A checkbox is ticked or not.

The code is checked by the real TypeScript compiler each time you type. Only line 3 changes. Reset puts "checkbox" back.

Every kind except "checkbox" gives string, even the type string itself. Without quotes, checkbox isn't a kind at all, so TypeScript reports it. The union "checkbox" | "email" gets both answers; a later section explains why.

A card "checkbox" goes into a card that asks: fits "checkbox"? A yes arrow leads up to a card that says boolean. A no arrow leads down to a card that says string.Kind"checkbox"fits "checkbox"?the questionyesbooleannostring
FieldValue<"checkbox"> asks one question: does "checkbox" fit "checkbox"? It does, so the answer is boolean. Any other kind takes the no arrow, to string.
Real-life analogyA mail sorter with one slot

At a post office, every piece of mail meets the same slot. A letter slides through and lands in the letter tray. Anything bigger stays out and goes to the parcel shelf. Nobody opens the mail; the shape alone decides where it goes.

In real life: The sorter asks one question: does it fit the letter slot?
In TypeScript: Kind extends "checkbox"
In real life: It fits, so it goes to the letter tray
In TypeScript: ? boolean
In real life: It doesn't fit, so it goes to the parcel shelf
In TypeScript: : string
In real life: A bag of mixed mail is sorted one piece at a time
In TypeScript: A union is checked one member at a time
The main idea

A conditional type picks one of two types. A extends B ? X : Y means "if A fits B, the type is X; otherwise it's Y". TypeScript makes the choice while it checks your code, and the choice is removed with the other types before the code runs.

extends asks: does it fit?

In a conditional type, A extends B asks: "can every A be used where a B is allowed?" TypeScript calls this being assignable. It doesn't mean "the same as". Four questions:

Four questions about fittingPop out in the code editor (opens in a new tab)TypeScript
type A = "Tea" extends string ? "yes" : "no";//   ^?type B = string extends "Tea" ? "yes" : "no";//   ^?type C = { name: string; price: number } extends { name: string } ? "yes" : "no";//   ^?type D = { name: string } extends { name: string; price: number } ? "yes" : "no";//   ^?
  • Line 1: "Tea" is a string, so it fits string. The query on line 2 shows "yes".
  • Line 3 turns it round. A string could be "Cake", so not every string fits "Tea". Line 4 shows "no".
  • Line 5: an object with a name and a price has everything { name: string } asks for. Extra properties are fine (the Structural typing lesson explains why). Line 6 shows "yes".
  • Line 7: an object with only a name is missing the price, so it doesn't fit. Line 8 shows "no".
Yes or no?
  • type R = "Tea" extends string ? "yes" : "no";
  • type R = string extends "Tea" ? "yes" : "no";
  • type R = 3 extends number ? "yes" : "no";
  • type R = number extends 3 | 4 ? "yes" : "no";
  • type R = { name: string; price: number } extends { name: string } ? "yes" : "no";
  • type R = { name: string } extends { price: number } ? "yes" : "no";
  • type R = string[] extends unknown[] ? "yes" : "no";
  • type R = "Tea" | 3 extends string ? "yes" : "no";
Try it yourself
0 of 8 correct

Sort each line by the answer TypeScript gives for R. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

You have met extends before, in angle brackets, where it is a rule (the Generic constraints lesson). In a conditional type it is a question. The difference shows when a type doesn't fit:

extends as a rule, then as a question (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function showLabel<T extends string>(label: T) {  console.log(label);}showLabel(3); type IsText<T> = T extends string ? "yes" : "no";type Three = IsText<3>;//   ^?

Line 1 says T extends string in angle brackets: a rule, called a constraint, that label must be text. Line 4 passes 3, so TypeScript reports Argument of type 'number' is not assignable to parameter of type 'string', on purpose here. Line 6 uses T extends string as a question. Line 7 passes 3 too, and nothing is wrong: the query on line 8 just shows the no answer, "no".

The table below compares the two. Read each row as: where extends is written, what it means there, and what happens when a type doesn't fit.

extends as a rule and as a question
Where extends is writtenWhat it meansWhen the type doesn't fit
In angle brackets: <T extends string>A rule, called a constraint: T must be a string.An error, like Argument of type 'number' is not assignable to parameter of type 'string'.
Before ?: T extends string ? "yes" : "no"A question, in a conditional type: is T a string?No error. The answer is the no branch, "no".

Conditional types with type parameters

A question about two fixed types, like "Tea" extends string, always has the same answer, so you could just write the answer. Conditional types are useful with a type parameter, where the answer depends on the type passed in.

Say you want the item type of an array. The Indexed access types lesson used Cart[number]. With a type parameter, that is an error on purpose here:

An item type for any T (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type ItemOf<T> = T[number];

TypeScript says Type 'number' cannot be used to index type 'T'. T could be any type, like boolean, which has no items to look up. A conditional type can check first:

The item type of an array, or the type itselfPop out in the code editor (opens in a new tab)TypeScript
type Flatten<T> = T extends unknown[] ? T[number] : T; type Tag = Flatten<string[]>;//   ^?type Count = Flatten<number>;//   ^?

Line 1 asks whether T fits unknown[], an array of anything. In the yes branch, TypeScript knows T is an array, so T[number] is allowed. In the no branch, Flatten gives T back unchanged. Line 3 passes a list of strings, and the query on line 4 shows the item type, string. Line 5 passes number, which isn't an array, and line 6 shows number.

A card T goes into a card that asks: T fits unknown[]? A yes arrow leads to a window called yes, holding T[number] and the note T is an array here. A no arrow leads to a card T, with the note T, unchanged.TT fits unknown[]?yesyesT[number]T is an array herenoTT, unchanged
Inside the yes branch, TypeScript knows T is an array, so T[number] is allowed. In the no branch, Flatten gives T back unchanged.

The same works for any property. Here is a type that reads the message from an API error, if it has one:

An error message, if the error has onePop out in the code editor (opens in a new tab)TypeScript
type ErrorText<T> = T extends { message: string } ? T["message"] : "Unknown error"; type Declined = ErrorText<{ message: "Card declined"; code: number }>;//   ^?type ServerDown = ErrorText<{ status: 500 }>;//   ^?

Line 1 asks whether T has a message that is text. If so, it looks the message up with T["message"]; if not, the answer is the text type "Unknown error". Line 3's error has a message, and the query on line 4 shows "Card declined". Line 5's error has only a status, and line 6 shows "Unknown error".

One question gives two answers. For more, put another conditional type in the no branch. Each : leads to the next question:

Three answers from two questionsPop out in the code editor (opens in a new tab)TypeScript
type FieldValue<Kind> =  Kind extends "checkbox" ? boolean :  Kind extends "number" ? number :  string; type Age = FieldValue<"number">;//   ^?type Subscribe = FieldValue<"checkbox">;//   ^?type Email = FieldValue<"email">;//   ^?

Lines 1 to 4 are one type. Line 2 asks about "checkbox"; if no, line 3 asks about "number"; if no again, line 4 gives string. The queries on lines 7, 9, and 11 show number, boolean, and string. TypeScript reads the questions in order and stops at the first yes, like else if.

Unions are checked one member at a time

In the overview, the union "checkbox" | "email" got both answers. When you pass a union to a type parameter that stands alone before extends, TypeScript splits the union, asks the question for each member, and joins the answers:

A union of two kinds gets two answersPop out in the code editor (opens in a new tab)TypeScript
type FieldValue<Kind> = Kind extends "checkbox" ? boolean : string; type Mixed = FieldValue<"checkbox" | "email">;//   ^?

Line 3 passes "checkbox" | "email". TypeScript works out FieldValue<"checkbox">, which is boolean, and FieldValue<"email">, which is string. The query on line 4 shows the two joined: string | boolean. This is called a distributive conditional type: the question is handed out to each member, the way you hand out one card to each person at a table.

The union "checkbox" | "email" is split into two cards. "checkbox" goes through the question fits "checkbox"? and gets boolean. "email" goes through the same question and gets string. The answers are joined into string | boolean."checkbox" | "email""checkbox"fits "checkbox"?yesboolean"email"fits "checkbox"?nostringjoined into one unionstring | boolean
FieldValue<"checkbox" | "email"> splits the union, asks the question once for each member, and joins the answers: string | boolean.

Splitting is useful for filtering a union. The answer never means "no type at all" (the any, unknown & never lesson covers it), and a union drops it, because it adds nothing. So answer never for the members you don't want:

Keep only the kinds that hold textPop out in the code editor (opens in a new tab)TypeScript
type FieldKind = "text" | "email" | "checkbox" | "number"; type TextLike<Kind> = Kind extends "text" | "email" ? Kind : never; type TextKinds = TextLike<FieldKind>;//   ^?

Line 1 lists four field kinds. Line 3 keeps a kind if it fits "text" | "email", and answers never otherwise. Line 5 passes all four. "checkbox" and "number" become never and disappear, and the query on line 6 shows "text" | "email". TypeScript's own Extract and Exclude types work this way; you'll meet them in the ReturnType, Awaited & more lesson. Change what line 3 keeps:

Playground: keep some field kinds
Keep the kinds that fitPop out in the code editor (opens in a new tab)TypeScript
type FieldKind = "text" | "email" | "checkbox" | "number"; type Picked<Kind> = Kind extends "text" | "email" ? Kind : never; type Result = Picked<FieldKind>;//   ^?
Line 3 keeps"text" | "email"
What TypeScript says
Result"text" | "email"
Try it yourself

"text" and "email" fit and are kept. "checkbox" and "number" don't fit, so they become never and disappear from the union.

TypeScript checks the code for real each time. Each button changes only what line 3 keeps. Reset goes back to the first button.

Each button changes only line 3, and the query on line 6 always shows the members that fit. When none fit, every member becomes never, and so does the result.

Check a union as a whole

Sometimes you want one answer about the whole union. Wrap both sides of extends in square brackets. [T] is a tuple with one item (the Arrays & tuples lesson covers tuples), so T no longer stands alone, and the union isn't split:

Split into two arrays, or kept as onePop out in the code editor (opens in a new tab)TypeScript
type ToArray<T> = T extends unknown ? T[] : never;type Split = ToArray<string | number>;//   ^? type ToArrayWhole<T> = [T] extends [unknown] ? T[] : never;type Whole = ToArrayWhole<string | number>;//   ^?

Line 1 asks T extends unknown. Every type fits unknown, so the answer is always T[]; the question is only there to split the union. Line 2 passes string | number, and the query on line 3 shows string[] | number[]: a list of strings or a list of numbers. Line 5 wraps both sides: [T] extends [unknown]. Line 6 passes the same union, and the query on line 7 shows (string | number)[]: one list that can hold both.

One card holds ["email" | 3]. It goes into one question, fits [string]?, and a no arrow leads to a card that says "no".["email" | 3]one wrapped unionfits [string]?one questionno"no"one answer
With square brackets around T, the union "email" | 3 is asked about once, as a whole. Not all of it is text, so the answer is "no".

The two forms can give quite different answers. Here IsText asks three questions. Press the buttons to switch line 1 between the two forms:

Playground: split the union or keep it whole
Three questions for IsTextPop out in the code editor (opens in a new tab)TypeScript
type IsText<T> = T extends string ? "yes" : "no"; type One = IsText<"email">;//   ^?type Mixed = IsText<"email" | 3>;//   ^?type Nothing = IsText<never>;//   ^?
Line 1 checksT extends string
The answers
"email""yes"
"email" | 3"yes" | "no"
nevernever
Try it yourself

T stands alone, so a union is split. "email" | 3 asks twice and gets "yes" | "no". never is an empty union, so there is nothing to ask, and the answer is never.

TypeScript checks the code for real each time. Each button changes only line 1. Reset goes back to T extends string.

For "email", both forms say "yes". For "email" | 3, the split form asks twice and gets "yes" | "no", while the whole form asks once and says "no". For never, the split form has no members to ask, so the answer is never, while the whole form says "yes", because never fits every type.

The table below puts the answers side by side. Read each row as: the input, then the answer from each form.

The same input, split and whole
The inputWith T extends stringWith [T] extends [string]
"email""yes", from one question."yes", from one question.
"email" | 3"yes" | "no": each member is asked on its own."no": the union is asked once, and it isn't all text.
nevernever: an empty union has no members to ask."yes": never fits every type.

A form reader whose return type follows the field

A form helper reads a field and gives back its value: true or false for a checkbox, a number for a number box, and text for the rest. A conditional type can say that in the return type. Written plainly, the function gets errors, on purpose here:

A form reader without assertions (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type FieldKind = "text" | "number" | "checkbox";type FieldValue<Kind> = Kind extends "checkbox" ? boolean : Kind extends "number" ? number : string;type Input = { value: string; checked: boolean }; function readField<Kind extends FieldKind>(kind: Kind, input: Input): FieldValue<Kind> {  if (kind === "checkbox") return input.checked;  if (kind === "number") return Number(input.value);  return input.value;}

Lines 1 to 3 describe the field kinds, the value for each kind, and an input with a value and a checked. Line 5 says readField gives back FieldValue<Kind>. Lines 6, 7, and 8 each get an error like Type 'boolean' is not assignable to type 'FieldValue<Kind>'. Inside the function, Kind isn't known yet, so TypeScript can't pick a branch, and checking kind === "checkbox" doesn't tell it which one either.

The usual fix is a type assertion, as FieldValue<Kind>, which tells TypeScript "trust me, this value has that type" (the Type assertions lesson covers as). It moves the checking to you, so keep each branch short:

A form reader whose return type follows the fieldPop out in the code editor (opens in a new tab)TypeScript
type FieldKind = "text" | "number" | "checkbox";type FieldValue<Kind> = Kind extends "checkbox" ? boolean : Kind extends "number" ? number : string;type Input = { value: string; checked: boolean }; function readField<Kind extends FieldKind>(kind: Kind, input: Input): FieldValue<Kind> {  if (kind === "checkbox") return input.checked as FieldValue<Kind>;  if (kind === "number") return Number(input.value) as FieldValue<Kind>;  return input.value as FieldValue<Kind>;} const ageBox = { value: "30", checked: false };const age = readField("number", ageBox);//    ^?const subscribed = readField("checkbox", { value: "on", checked: true });//    ^?console.log(age + 1, subscribed);

Lines 6 to 8 add as FieldValue<Kind>, and the errors are gone. The payoff is at each call. Line 12 reads a number box, and the query on line 13 shows const age: number, so age + 1 is safe. Line 14 reads a checkbox, and line 15 shows boolean. Line 16 prints 31 true. Each caller gets the exact type for its field, with no assertion of its own.

Common mix-ups

Only a type parameter that stands alone splits a union. Write the union out, and it is asked about once:

A union through a type parameter, and written outPop out in the code editor (opens in a new tab)TypeScript
type IsText<T> = T extends string ? "yes" : "no"; type ViaParameter = IsText<string | number>;//   ^?type WrittenOut = string | number extends string ? "yes" : "no";//   ^?

Line 3 passes string | number to IsText, so the union is split, and the query on line 4 shows "yes" | "no". Line 5 writes the same question with the union itself before extends. There is no type parameter, so nothing is split: the whole union isn't text, and line 6 shows "no".

  • "extends here means one class inherits from another." No. In a conditional type it asks whether one type fits another, for any types: "Tea" extends string is yes.
  • "A extends B asks whether the two types are the same." No. It asks whether every A can be used as a B. string extends "Tea" is no, but "Tea" extends string is yes.
  • "A conditional type runs like an if while the page runs." No. TypeScript picks the answer as it checks your code, and the type is removed before the code runs.
  • "Every union is split." No. Only a type parameter that stands alone before extends splits a union. A union written out, or wrapped as [T], is asked about once.
  • "never takes the no branch." No. With a split, never is an empty union, so the answer is never.
  • "TypeScript checks a function's body against a conditional return type." No. Inside the function it can't pick a branch, so you write as, and checking each branch is up to you.

Practice exercises

Each exercise is TypeScript. Press Edit & run on the starter code: the errors and ^? answers update as you type. Try a guess first, then use the hints.

Exercise 1 · Warm-upPick the branch

What type is Cart? Guess first. Then press Edit & run and add a // ^? line under it to check.

Starter codePop out in the code editor (opens in a new tab)TypeScript
type IsList<T> = T extends unknown[] ? "list" : "single"; type Cart = IsList<string[]>;

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upDoes it fit?

    What type is Fits?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Fits = { id: number } extends { id: number; name: string } ? "yes" : "no";

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead the data from an API reply

      Replace unknown on line 1 so ApiData gives the data of a reply that has one, and never otherwise. What does the query on line 6 show then?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      type ApiData<T> = unknown; // write a conditional type here type User = ApiData<{ data: { name: string }; status: 200 }>;//   ^?type Failed = ApiData<{ error: string; status: 500 }>;//   ^?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the line TypeScript marks

        Two types that look up items. Which line will TypeScript mark?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type First<T> = T extends unknown[] ? T[0] : T;
        type Last<T> = T[number];

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeKeep only the prices

          Change line 1 so OnlyNumbers keeps only the numbers from a union. What type is Prices then?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type OnlyNumbers<T> = T; // keep only the numbers type Prices = OnlyNumbers<3 | "Tea" | 4>;//   ^?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeAsk about the whole union

            AllText should answer false when any member isn't text, but the query on line 4 shows boolean. Why? Change line 1 so it asks about the whole union. What type is Check then?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type AllText<T> = T extends string ? true : false; type Check = AllText<"Tea" | 3>;//   ^?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, find the question the type asks, then ask: is a union split here? The quiz shows its code without the checker, so the answers aren't shown on the page.

              The conditional types quiz · 7 questionsScore: first tries count
              1. Question 1 of 7How do you read A extends B ? X : Y?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which answer does this check of string against "Tea" give?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Answer = string extends "Tea" ? "yes" : "no";

                Choose an answer to see the explanation.

              3. Question 3 of 7What type is Flags, flattened from a list of booleans?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Flatten<T> = T extends unknown[] ? T[number] : T;
                
                type Flags = Flatten<boolean[]>;

                Choose an answer to see the explanation.

              4. Question 4 of 7What type is Lists when ToArray gets a union?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type ToArray<T> = T extends unknown ? T[] : never;
                
                type Lists = ToArray<string | number>;

                Choose an answer to see the explanation.

              5. Question 5 of 7What type is List when the square brackets keep the union whole?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type ToArray<T> = [T] extends [unknown] ? T[] : never;
                
                type List = ToArray<string | number>;

                Choose an answer to see the explanation.

              6. Question 6 of 7Which kinds does TextLike keep from this union?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type TextLike<Kind> = Kind extends "text" | "email" ? Kind : never;
                
                type Kept = TextLike<"text" | "checkbox">;

                Choose an answer to see the explanation.

              7. Question 7 of 7When does a conditional type pick its answer?

                Choose an answer to see the explanation.

              Key takeaways

              • A extends B ? X : Y picks X if A fits B, and Y otherwise.
              • "Fits" means every A can be used as a B: "Tea" extends string is yes, the other way round is no.
              • In angle brackets, extends is a rule that reports errors. Before ?, it is a question that picks an answer.
              • Conditional types are useful with type parameters. In the yes branch, TypeScript knows more, so lookups like T[number] work.
              • A union passed to a type parameter that stands alone is split, asked member by member, and joined. Answer never to drop members.
              • [T] extends [U] asks about the whole union once.
              • The choice is made while TypeScript checks, not while the page runs.

              Remember the one-liner.
              A conditional type asks "does it fit?" and answers with a type, one union member at a time unless you wrap it.

              Coming next: The infer keyword. It lets the question in a conditional type capture a part of the type, like a function's return type, and use it in the answer.

              CompleteFrontend Clear concepts. Working examples.