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.
- 01Read and write a conditional typeRead
T extends U ? X : Yas a question, "does T fit U?", and pick the answer it gives. - 02Use it with a type parameterWrite types like
Flatten<T>andFieldValue<Kind>whose answer depends on the type passed in. - 03Control 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):
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:
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: "doesKindfit"checkbox"?"? booleanis the answer for yes. A checkbox is ticked or not, so its value is true or false.: stringis 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.
type FieldValue<Kind> = Kind extends "checkbox" ? boolean : string; type Looked = FieldValue<"checkbox">;// ^?"checkbox"booleanNo errors"checkbox" fits "checkbox", so the answer is the yes branch: boolean. A checkbox is ticked or not.
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.
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.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
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:
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 fitsstring. The query on line 2 shows"yes". - Line 3 turns it round. A
stringcould 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".
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";
Sort each line by the answer TypeScript gives for R. Each line is checked on its own.
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:
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.
| Where extends is written | What it means | When 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:
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:
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.
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:
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:
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:
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.
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:
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:
type FieldKind = "text" | "email" | "checkbox" | "number"; type Picked<Kind> = Kind extends "text" | "email" ? Kind : never; type Result = Picked<FieldKind>;// ^?"text" | "email""text" | "email""text" and "email" fit and are kept. "checkbox" and "number" don't fit, so they become never and disappear from the union.
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:
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.
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:
type IsText<T> = T extends string ? "yes" : "no"; type One = IsText<"email">;// ^?type Mixed = IsText<"email" | 3>;// ^?type Nothing = IsText<never>;// ^?T extends string"yes""yes" | "no"neverT 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.
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 input | With T extends string | With [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. |
never | never: 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:
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:
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:
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".
- "
extendshere means one class inherits from another." No. In a conditional type it asks whether one type fits another, for any types:"Tea" extends stringis yes. - "
A extends Basks whether the two types are the same." No. It asks whether everyAcan be used as aB.string extends "Tea"is no, but"Tea" extends stringis yes. - "A conditional type runs like an
ifwhile 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
extendssplits a union. A union written out, or wrapped as[T], is asked about once. - "
nevertakes the no branch." No. With a split,neveris an empty union, so the answer isnever. - "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.
What type is Cart? Guess first. Then press Edit & run and add a // ^? line under it to check.
type IsList<T> = T extends unknown[] ? "list" : "single"; type Cart = IsList<string[]>;type IsList<T> = T extends unknown[] ? "list" : "single"; type Cart = IsList<string[]>;// ^?string[] fits unknown[], so IsList takes the yes branch. The query on line 4 shows type Cart = "list".
What type is Fits?
type Fits = { id: number } extends { id: number; name: string } ? "yes" : "no";type Fits = { id: number } extends { id: number; name: string } ? "yes" : "no";// ^?{ id: number } has no name, so it can't be used where { id: number; name: string } is needed. The answer is "no".
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?
type ApiData<T> = unknown; // write a conditional type here type User = ApiData<{ data: { name: string }; status: 200 }>;// ^?type Failed = ApiData<{ error: string; status: 500 }>;// ^?type ApiData<T> = T extends { data: unknown } ? T["data"] : never; type User = ApiData<{ data: { name: string }; status: 200 }>;// ^?type Failed = ApiData<{ error: string; status: 500 }>;// ^?A reply with data takes the yes branch, so User is { name: string }. The failed reply has no data, so Failed is never.
Two types that look up items. Which line will TypeScript mark?
type First<T> = T extends unknown[] ? T[0] : T;
type Last<T> = T[number];type First<T> = T extends unknown[] ? T[0] : T;type Last<T> = T extends unknown[] ? T[number] : T;Line 2 looks up T[number] with no check, so TypeScript says Type 'number' cannot be used to index type 'T'. Asking T extends unknown[] first makes the lookup safe.
Change line 1 so OnlyNumbers keeps only the numbers from a union. What type is Prices then?
type OnlyNumbers<T> = T; // keep only the numbers type Prices = OnlyNumbers<3 | "Tea" | 4>;// ^?type OnlyNumbers<T> = T extends number ? T : never; type Prices = OnlyNumbers<3 | "Tea" | 4>;// ^?T stands alone, so the union is split. 3 and 4 fit number and are kept; "Tea" becomes never and disappears. Prices is 3 | 4.
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?
type AllText<T> = T extends string ? true : false; type Check = AllText<"Tea" | 3>;// ^?type AllText<T> = [T] extends [string] ? true : false; type Check = AllText<"Tea" | 3>;// ^?Split, the answers are true | false, which TypeScript shows as boolean. With [T] extends [string], the union is asked once. It isn't all text, so Check is false.
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.
Question 1 of 7How do you read
A extends B ? X : Y?Choose an answer to see the explanation.
Question 2 of 7Which answer does this check of string against "Tea" give?
Read the code, then predicttype Answer = string extends "Tea" ? "yes" : "no";Choose an answer to see the explanation.
Question 3 of 7What type is
Flags, flattened from a list of booleans?Read the code, then predicttype Flatten<T> = T extends unknown[] ? T[number] : T; type Flags = Flatten<boolean[]>;Choose an answer to see the explanation.
Question 4 of 7What type is
Listswhen ToArray gets a union?Read the code, then predicttype ToArray<T> = T extends unknown ? T[] : never; type Lists = ToArray<string | number>;Choose an answer to see the explanation.
Question 5 of 7What type is
Listwhen the square brackets keep the union whole?Read the code, then predicttype ToArray<T> = [T] extends [unknown] ? T[] : never; type List = ToArray<string | number>;Choose an answer to see the explanation.
Question 6 of 7Which kinds does TextLike keep from this union?
Read the code, then predicttype TextLike<Kind> = Kind extends "text" | "email" ? Kind : never; type Kept = TextLike<"text" | "checkbox">;Choose an answer to see the explanation.
Question 7 of 7When does a conditional type pick its answer?
Choose an answer to see the explanation.
Key takeaways
A extends B ? X : YpicksXifAfitsB, andYotherwise.- "Fits" means every
Acan be used as aB:"Tea" extends stringis yes, the other way round is no. - In angle brackets,
extendsis 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
neverto 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.