Discriminated unions
Model loading, success, and error states with a shared tag field, so TypeScript stops you reading data that isn't there. With playgrounds to break.
- 01Tag each shapeGive every object in a union the same field with its own exact value, and check that field to get one shape.
- 02Switch on the tagHandle each shape in its own case, where TypeScript knows exactly which fields are there.
- 03Model states safelyDescribe loading, success, and error as one type, so states that can't happen can't be written.
Reading data that isn't there yet
A shop page asks a server for its products. While it waits, the page is loading. Then the request either succeeds, and the page has a list of products, or it fails, and the page has an error message. Here is a plain JavaScript function that counts the products:
function productCount(state) {
return `${state.products.length} products`;
}
console.log(productCount({ status: "success", products: ["Tea", "Cake"] }));
console.log(productCount({ status: "error", message: "Network down" }));Line 1 takes a state object. Line 2 reads state.products.length. Line 5 passes a successful state with two products and prints 2 products. Line 6 passes a failed state, which has a message and no products. So state.products is undefined, reading .length from it throws a TypeError, and the code stops. Nothing warned you before it ran.
In TypeScript you describe the three states as one union type: a type that can be one of several types, written with |, which you read as "or". Each state is an object type with its own fields:
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function productCount(state: ProductsState) { return `${state.products.length} products`;} console.log(productCount({ status: "success", products: ["Tea", "Cake"] }));Lines 1 to 4 make a type called ProductsState. A state is one of three objects. Each one has a status field, and in each, status has one exact value. In a type, "loading" in quotes means "only the text loading", not any string. A type that is one exact value is called a literal type (the Literal types lesson covers them). Line 3 says a successful state also has products, a list of strings, and line 4 says a failed one has a message.
Line 7 reads state.products, and TypeScript marks it on purpose: Property 'products' does not exist on type 'ProductsState'. That means "not every state has products". The message's second line names one that doesn't: { status: "loading"; }. Line 10 still prints 2 products when you run it, but TypeScript has seen the crash coming. Check the status first:
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function productCount(state: ProductsState) { if (state.status === "success") { return `${state.products.length} products`; // ^? } return "No products yet";} console.log(productCount({ status: "success", products: ["Tea", "Cake"] }));console.log(productCount({ status: "error", message: "Network down" }));Line 7 asks whether state.status is "success". Only one shape has that status, so inside the if, TypeScript keeps only that shape. The query on line 9 shows it: state: { status: "success"; products: string[]; }. Line 8 may read the products, and the error is gone. Line 14 prints 2 products, and line 15 prints No products yet instead of throwing.
status. Checking status picks one shape, so on line 8 TypeScript knows products is there.In a shop's stockroom, every box has a label on the front: shoes, hats, or gloves. You don't open a box and feel around for shoe sizes. You read the label first, and then you know exactly what's inside and what to look for.
- In real life: Every box in the stockroom has a label on the front
- In TypeScript: Every state has a
statusfield - In real life: A label says one word from a short list: shoes, hats, or gloves
- In TypeScript:
statusis"loading","success", or"error" - In real life: A box labelled shoes always holds shoes, with their sizes
- In TypeScript: A
"success"state always holdsproducts - In real life: You read the label before you reach inside
- In TypeScript: You check
statusbefore you readproducts
A discriminated union is a union of object types that all share one field, called the tag, and each gives it a different exact value, like status: "success". Check the tag, and TypeScript knows which shape you have and which fields are there.
A shared field that names the shape
"Discriminate" means "tell apart". The field that tells the shapes apart is the tag, also called the discriminant. For a check on the tag to pick exactly one shape:
- every shape in the union has the field, with the same name;
- each shape gives it an exact value, a literal type;
- no two shapes use the same value.
Here is a discount at checkout. It takes a percent off, or a fixed amount off:
type Discount = | { kind: "percent"; percent: number } | { kind: "fixed"; amount: number }; function priceAfter(discount: Discount, price: number) { if (discount.kind === "percent") { return price - (price * discount.percent) / 100; // ^? } return price - discount.amount; // ^?} console.log(priceAfter({ kind: "percent", percent: 10 }, 20));console.log(priceAfter({ kind: "fixed", amount: 5 }, 20));Lines 1 to 3 say a Discount is one of two shapes. The tag is kind. A percent discount has a percent, and a fixed discount has an amount. Line 6 checks the kind. Inside the if, the query on line 8 shows discount is the percent shape, so line 7 may read discount.percent.
Line 7 also returns, so the code after the if only runs for the other shape. The query on line 11 shows discount is the fixed shape there, and line 10 reads discount.amount. Line 14 prints 18, which is 10% off 20, and line 15 prints 15, which is 5 off 20.
The tag can have any name. kind, type, and status are common. Its values can be text, numbers, or true and false. What matters is that each value is exact. Watch what happens when the tag is any string:
type Discount = | { kind: string; percent: number } | { kind: string; amount: number }; function priceAfter(discount: Discount, price: number) { if (discount.kind === "percent") { return price - (price * discount.percent) / 100; } return price - discount.amount;} console.log(priceAfter({ kind: "percent", percent: 10 }, 20));The only change is on lines 2 and 3: kind is a string in both shapes. Now "percent" fits both, so line 6 can't pick one. TypeScript marks line 7, on purpose: Property 'percent' does not exist on type 'Discount', because a fixed discount could get inside the if. Line 9 gets the same error for amount. The code runs and prints 18, but TypeScript can no longer check it for you.
Now change the check yourself. Type a different kind for line 6, and watch the list under the code as you type:
type Discount = | { kind: "percent"; percent: number } | { kind: "fixed"; amount: number }; function priceAfter(discount: Discount, price: number) { if (discount.kind === "percent") { return price - (price * discount.percent) / 100; } return price - discount.amount;} console.log(priceAfter({ kind: "percent", percent: 10 }, 20));"percent"No errorsprints 18Line 6 checks for a percent discount. Inside the if, TypeScript knows discount has percent, and after it, only the fixed discount with its amount is left. No errors, and the code prints 18: 10% off 20.
With "fixed", the two branches swap shapes, and the reads on lines 7 and 9 no longer match: two errors. With the typo "percnt", TypeScript marks line 6 itself, because no discount can have that kind. That's a bug caught on the check, before anyone reads the wrong field. Put "percent" back and the errors go away.
The in & instanceof lesson told shapes apart with in, which asks whether an object has a field. That works too, but a tag is clearer. The table compares the two. Read each row as a question, then each check's answer.
| Question | Check a field with in | Check the tag |
|---|---|---|
| What do you write? | "products" in state | state.status === "success" |
| What happens when you misspell it? | The check itself gets no error. The confusion shows up on a later line. | The check gets an error right away: the two types have no overlap. |
| Can it tell apart two shapes with the same fields? | No. If both shapes have a message, in matches both. | Yes. Each shape has its own tag value, even when the other fields match. |
Can one switch handle every shape? | No. Each in check needs its own if. | Yes. Each case names one tag value. |
switch on the tag
With three or more shapes, a chain of if checks gets long. A switch on the tag reads better: it compares one value with each case and runs the one that matches. TypeScript narrows inside every case. Here is a shopping cart that describes what the shopper just did:
type CartAction = | { type: "add"; name: string; price: number } | { type: "remove"; name: string } | { type: "clear" }; function describe(action: CartAction) { switch (action.type) { case "add": return `Added ${action.name} for $${action.price}`; case "remove": return `Removed ${action.name}`; // ^? case "clear": return "Emptied the cart"; }} console.log(describe({ type: "add", name: "Tea", price: 3 }));console.log(describe({ type: "remove", name: "Tea" }));console.log(describe({ type: "clear" }));Lines 1 to 4 describe three cart actions. Adding needs a name and a price, removing needs only a name, and clearing needs nothing else. The tag is type. Line 7 switches on action.type.
- In
case "add":, line 9 reads the name and the price. - In
case "remove":, line 11 reads the name. The query on line 12 showsaction: { type: "remove"; name: string; }: no price here. - In
case "clear":, line 14 needs nothing from the action.
Lines 18 to 20 print Added Tea for $3, Removed Tea, and Emptied the cart.
action.type once and jumps to the matching case. Inside each case, TypeScript keeps only the action with that tag: case "add" can read a name and a price, and case "clear" has nothing else to read.Two cases can share one body. TypeScript then keeps both shapes:
type CartAction = | { type: "add"; name: string; price: number } | { type: "remove"; name: string } | { type: "clear" }; function itemName(action: CartAction) { switch (action.type) { case "add": case "remove": return action.name; // ^? case "clear": return "every item"; }} console.log(itemName({ type: "remove", name: "Cake" }));console.log(itemName({ type: "clear" }));Lines 8 and 9 list "add" and "remove" with nothing between them, so both run line 10. The query on line 11 shows action is the add shape or the remove shape. Both have a name, so line 10 may read it. Line 17 prints Cake and line 18 prints every item.
Now send the function different actions. Each button changes the object on line 17:
type CartAction = | { type: "add"; name: string; price: number } | { type: "remove"; name: string } | { type: "clear" }; function describe(action: CartAction) { switch (action.type) { case "add": return `Added ${action.name} for $${action.price}`; case "remove": return `Removed ${action.name}`; case "clear": return "Emptied the cart"; }} console.log(describe({ type: "add", name: "Tea", price: 3 }));Added Tea for $3case "add"No errorsThe tag is "add", so the switch runs lines 8 and 9. In that case TypeScript knows the action has a name and a price. It prints Added Tea for $3.
The tag decides two things. While the page runs, it decides which case runs: the highlighted lines. Before it runs, it decides what TypeScript expects: an add action must have a price, so Add with no price gets an error and prints $undefined. And A refund isn't one of the three tags, so TypeScript marks it before it can fall through every case and print undefined.
Loading, success, and error as one type
Almost every page that loads data goes through the same moments: waiting for the reply, showing the data, or showing what went wrong. ProductsState describes them, and the function that loads the products gives back one of them:
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; async function loadProducts(): Promise<ProductsState> { try { const response = await fetch("/api/products"); const products: string[] = await response.json(); return { status: "success", products }; } catch (error) { return { status: "error", message: String(error) }; }}This code needs a real server at /api/products, so it has no Run button here. Line 6 says loadProducts gives back a Promise<ProductsState>: a promise that later hands over a ProductsState (async and await covers promises). Line 8 asks the server for the products, and line 9 reads the list from its reply. Line 10 returns a success state with that list. If anything goes wrong, line 12 returns an error state with a message. Each return builds one shape, and TypeScript checks it against the shape its tag names. The page starts as { status: "loading" } and shows whatever comes back.
Now put the page in each state. Lines 6 to 15 turn a state into what the page shows. Line 17 stores a state, and line 18 prints what the page shows for it. Each button changes line 17:
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function render(state: ProductsState) { switch (state.status) { case "loading": return "Loading products"; case "success": return state.products.join(", "); case "error": return `Something went wrong: ${state.message}`; }} const state: ProductsState = { status: "loading" };console.log(render(state));Loading productsloadingNo errorsThe request hasn't answered yet. A loading state has nothing but its tag, and the page shows Loading products.
Loading, Loaded, and Failed each run their own case, and each case reads only the data its state has. Loaded, but no list says it succeeded without saying what it found. TypeScript checks it against the success shape and reports Property 'products' is missing. Without that check, line 11 would throw a TypeError on the page.
States that can't happen
Before discriminated unions, many pages kept one object with a loading flag and optional fields. In a type, a ? after a field's name makes it optional: the object may leave it out. (The Object types lesson covers this.) Here is the same product list written that way:
type ProductsState = { loading: boolean; products?: string[]; error?: string;}; function render(state: ProductsState) { if (state.loading) return "Loading products"; if (state.error) return `Something went wrong: ${state.error}`; return state.products.join(", ");} console.log(render({ loading: true, products: ["Tea", "Cake"], error: "Network down" }));console.log(render({ loading: false }));Lines 1 to 5 describe a state with a loading flag and two optional fields. Line 8 returns early while loading, and line 9 returns early when there's an error. You know that leaves a successful state with products. TypeScript doesn't: nothing in the type ties "not loading and no error" to "has products". So it marks line 10, on purpose: 'state.products' is possibly 'undefined'.
It's right to worry. Line 13 passes a state that is loading, has two products, and failed. TypeScript accepts it, and it prints Loading products. Line 14 passes a state that isn't loading, has no error, and has no products. TypeScript accepts that too, and line 10 throws a TypeError.
Count what the loose type allows. loading is true or false, and products and error are each there or missing: 2 × 2 × 2 = 8 combinations. Only 3 of them make sense. The union allows exactly those 3, so a state that can't happen can't be written:
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; const state: ProductsState = { status: "loading", products: ["Tea", "Cake"], message: "Network down",};Line 7 says the state is loading. A loading state has no other fields, so TypeScript marks line 8, on purpose: Object literal may only specify known properties, and 'products' does not exist in type '{ status: "loading"; }'. An object literal is an object written out in { }, so this means "this object has a field its shape doesn't list". TypeScript reports the first extra field it finds; remove products and line 9 gets the same error for message.
The table compares the two designs. Read each row as a question, then each design's answer.
| Question | Optional fields | A union of states |
|---|---|---|
| How many combinations does the type allow? | Eight: loading is true or false, and products and error are each there or missing. | Three: loading, success, or error. |
| Is loading, with products, with an error, allowed? | Yes, with no error at all. | No. A loading state has no products, so TypeScript marks it. |
| After you rule out loading and errors, can you read the products? | No. TypeScript still says 'state.products' is possibly 'undefined'. | Yes. Inside case "success":, products is always there. |
| What happens when a state leaves out its data? | Nothing. Every field is optional, so nothing is missing. | An error: Property 'products' is missing. |
A profile page and its API reply
A profile page asks the server for the signed-in user. Many APIs answer with an ok field: true with the data, or false with an error. That's a tag too, with true and false as its two exact values:
type User = { name: string; email: string };type UserReply = | { ok: true; user: User } | { ok: false; error: string }; function greeting(reply: UserReply) { if (reply.ok) { return `Welcome back, ${reply.user.name}`; } return `Could not load your profile: ${reply.error}`; // ^?} console.log(greeting({ ok: true, user: { name: "Asha", email: "asha@example.com" } }));console.log(greeting({ ok: false, error: "Signed out" }));Line 1 describes a User. Lines 2 to 4 say a reply is ok: true with a user, or ok: false with an error. Line 7 checks reply.ok. Inside, only the ok: true shape is left, so line 8 may read reply.user.name. After the if, the query on line 11 shows reply: { ok: false; error: string; }, and line 10 reads the error. Line 14 prints Welcome back, Asha, and line 15 prints Could not load your profile: Signed out.
Here is the function that builds those replies from a real request:
type User = { name: string; email: string };type UserReply = | { ok: true; user: User } | { ok: false; error: string }; async function loadUser(id: number): Promise<UserReply> { const response = await fetch(`/api/users/${id}`); if (!response.ok) { return { ok: false, error: `The server said ${response.status}` }; } const user: User = await response.json(); return { ok: true, user };}This code needs a real server, so it has no Run button here. Line 7 asks for the user with fetch. Line 8 checks the browser's own response.ok, which is false when the server answers with an error code like 404. Then line 9 returns a failed reply. Otherwise line 11 reads the user from the reply's JSON, and line 12 returns a successful one. Every page that calls loadUser has to check ok before it can reach the user.
reply.user.name;if (reply.ok) reply.user.name;if (!reply.ok) reply.error;if (reply.ok) reply.error;if (reply.ok === false) reply.error;const signedOut: UserReply = { ok: false, error: "Signed out" };const broken: UserReply = { ok: false };const mixed: UserReply = { ok: true, error: "Signed out" };
Sort each line by what TypeScript says about it. Each line is checked on its own, after the User and UserReply types above, with reply holding a UserReply.
Common mix-ups
A tag only helps when the data really has it. Here a server reply claims to be a success:
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; const state: ProductsState = JSON.parse('{ "status": "success" }');if (state.status === "success") { console.log(state.products.length);}Line 6 reads a state from a server reply with JSON.parse. JSON.parse gives back the type any, which TypeScript never checks, so it trusts the ProductsState type you wrote. Line 7 checks the tag, and TypeScript reports nothing. But the reply has no products, so line 8 throws a TypeError when it runs. The types are removed before the code runs, so nothing checks that the tag matches the fields.
- "The tag has to be called kind." No. Any name works, as long as every shape has it.
type,status, andokare common too. - "Any shared field works as a tag." No. Each shape has to give it an exact value. With
kind: stringin every shape, checking it can't pick one. - "Optional fields are just as safe." No. They allow states that can't happen, like loading with an error, and they make you check fields TypeScript can't connect.
- "TypeScript checks the tag while the page runs." No. It checks your code before it runs. A reply that says
"success"and has no products gets through. - "Discriminated unions need special syntax." No. They're ordinary unions of object types. The shared field with exact values is all it takes.
- "I can check state.products to find a successful state." No. Until the tag is checked, you can read only fields every shape has, like
status.if (state.products)is an error of its own, because a loading state has noproductsto read.
Practice exercises
Each exercise is TypeScript. When you fix or write code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
What type does discount have on line 7? Guess first. Then point at discount to check, or press Edit & run and add a // ^? line under it.
type Discount = | { kind: "percent"; percent: number } | { kind: "fixed"; amount: number }; function label(discount: Discount) { if (discount.kind === "fixed") { return `$${discount.amount} off`; } return `${discount.percent}% off`;}type Discount = | { kind: "percent"; percent: number } | { kind: "fixed"; amount: number }; function label(discount: Discount) { if (discount.kind === "fixed") { return `$${discount.amount} off`; // ^? } return `${discount.percent}% off`;}Line 6 keeps only the shape whose kind is "fixed", so on line 7 discount is { kind: "fixed"; amount: number }. The query on line 8 shows it, and that's why line 7 may read amount.
One case reads a field its state doesn't have. Which line will TypeScript mark?
type ProductsState =
| { status: "loading" }
| { status: "success"; products: string[] }
| { status: "error"; message: string };
function render(state: ProductsState) {
switch (state.status) {
case "loading":
return "Loading products";
case "success":
return state.message;
case "error":
return `Something went wrong: ${state.message}`;
}
}type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function render(state: ProductsState) { switch (state.status) { case "loading": return "Loading products"; case "success": return state.products.join(", "); case "error": return `Something went wrong: ${state.message}`; }}Line 11 reads state.message in case "success":, and a success state has products, not message. TypeScript says Property 'message' does not exist on type '{ status: "success"; products: string[]; }'. The fix reads the products instead.
A toast is a small message that pops up on a page, like "Saved". Some toasts have an Undo button. Give both toast types a tag, and make buttonText return the undo label for undo toasts and "Close" for the others. What does it print for the info toast?
type InfoToast = { text: string };type UndoToast = { text: string; undoLabel: string };type Toast = InfoToast | UndoToast; function buttonText(toast: Toast) { return toast.undoLabel;} console.log(buttonText({ text: "Saved" }));type InfoToast = { kind: "info"; text: string };type UndoToast = { kind: "undo"; text: string; undoLabel: string };type Toast = InfoToast | UndoToast; function buttonText(toast: Toast) { if (toast.kind === "undo") { return toast.undoLabel; } return "Close";} console.log(buttonText({ kind: "info", text: "Saved" }));console.log(buttonText({ kind: "undo", text: "Tea removed", undoLabel: "Undo" }));With kind: "info" and kind: "undo", checking the kind picks one shape, so line 7 may read undoLabel. The info toast skips the if and prints Close; the undo toast prints Undo.
A Save button keeps its state with a flag and optional fields, and TypeScript marks line 10. Rewrite SaveState as a discriminated union, and handle each state. What does your fixed code print?
type SaveState = { saving: boolean; savedAt?: string; error?: string;}; function saveLabel(state: SaveState) { if (state.saving) return "Saving"; if (state.error) return `Not saved: ${state.error}`; return `Saved at ${state.savedAt.slice(0, 5)}`;} console.log(saveLabel({ saving: false, savedAt: "14:30:05" }));type SaveState = | { status: "saving" } | { status: "saved"; savedAt: string } | { status: "failed"; error: string }; function saveLabel(state: SaveState) { switch (state.status) { case "saving": return "Saving"; case "saved": return `Saved at ${state.savedAt.slice(0, 5)}`; case "failed": return `Not saved: ${state.error}`; }} console.log(saveLabel({ status: "saved", savedAt: "14:30:05" }));In case "saved":, savedAt is always there, so the possibly 'undefined' error is gone. "14:30:05".slice(0, 5) keeps the first five characters, so it prints Saved at 14:30.
The product list is now a search box. Before the visitor types, nothing has been asked yet. Add an "idle" state that shows Type to search. How many states does ProductsState have after your change?
type ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function render(state: ProductsState) { switch (state.status) { case "loading": return "Loading products"; case "success": return state.products.join(", "); case "error": return `Something went wrong: ${state.message}`; }} console.log(render({ status: "idle" }));type ProductsState = | { status: "idle" } | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function render(state: ProductsState) { switch (state.status) { case "idle": return "Type to search"; case "loading": return "Loading products"; case "success": return state.products.join(", "); case "error": return `Something went wrong: ${state.message}`; }} console.log(render({ status: "idle" }));Line 2 adds the idle state, so { status: "idle" } is allowed, and lines 9 and 10 handle it. The type now has four states, and the code prints Type to search instead of undefined.
A newsletter form is being edited, sending, sent, or failed. The function is written; the type is missing. Write SignupState so the errors go away. What does the code print?
// Write SignupState here: editing, sending, sent (with the email), or failed (with a message). function statusText(state: SignupState) { switch (state.status) { case "editing": return "Enter your email"; case "sending": return "Sending"; case "sent": return `Check ${state.email} for a link`; case "failed": return `Try again: ${state.message}`; }} console.log(statusText({ status: "sent", email: "asha@example.com" }));type SignupState = | { status: "editing" } | { status: "sending" } | { status: "sent"; email: string } | { status: "failed"; message: string }; function statusText(state: SignupState) { switch (state.status) { case "editing": return "Enter your email"; case "sending": return "Sending"; case "sent": return `Check ${state.email} for a link`; case "failed": return `Try again: ${state.message}`; }} console.log(statusText({ status: "sent", email: "asha@example.com" }));Each case reads only its own state's field: email when sent, message when failed. With the type in place there are no errors, and the sent state prints Check asha@example.com for a link.
Check your understanding
For each question, ask which shape the code has at that line, and what that shape's fields are. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What makes a union of object types a discriminated union?
Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in this cart action code?
Read the code, then predicttype CartAction = | { type: "add"; name: string; price: number } | { type: "remove"; name: string }; function priceOf(action: CartAction) { if (action.type === "add") { return action.price; } return action.price; }Choose an answer to see the explanation.
Question 3 of 7What type does
statehave on line 9, inside case "error"?Read the code, then predicttype ProductsState = | { status: "loading" } | { status: "success"; products: string[] } | { status: "error"; message: string }; function render(state: ProductsState) { switch (state.status) { case "error": return state.message; default: return "All good"; } }Choose an answer to see the explanation.
Question 4 of 7Does TypeScript accept this state, which is loading and failed at once?
Read the code, then predicttype ProductsState = { loading: boolean; products?: string[]; error?: string; }; const state: ProductsState = { loading: true, error: "Network down" };Choose an answer to see the explanation.
Question 5 of 7Why does TypeScript mark line 7 in this notice code?
Read the code, then predicttype Notice = | { kind: string; text: string } | { kind: string; text: string; undoLabel: string }; function buttonText(notice: Notice) { if (notice.kind === "undo") { return notice.undoLabel; } return "Close"; }Choose an answer to see the explanation.
Question 6 of 7What happens when this server reply code runs?
Read the code, then predicttype UserReply = | { ok: true; name: string } | { ok: false; error: string }; const reply: UserReply = JSON.parse('{ "ok": true }'); if (reply.ok) { console.log(reply.name.toUpperCase()); }Choose an answer to see the explanation.
Question 7 of 7Which pair of fields can work as a tag?
Choose an answer to see the explanation.
Key takeaways
- A discriminated union is a union of object types that share one field, the tag, with a different exact value in each.
- Checking the tag with
===,!==, or a truthiness check narrows the whole object to one shape. - A
switchon the tag gives each shape its own case, and two cases can share one body. - The tag needs exact values: text, numbers, or
trueandfalse. A tag typed asstringcan't pick a shape. - Describe loading, success, and error as one union, so each state carries exactly its own data.
- Optional fields allow states that can't happen; a union rules them out before the code runs.
- Data from a server isn't checked while the page runs, so a wrong tag can still get through.
Remember the one-liner.
Give every shape the same tag with its own value, check the tag first, and TypeScript hands you exactly the fields that are there.
Coming next: Exhaustiveness checking. It makes TypeScript tell you when a switch forgets one of the union's shapes, like a new state you just added.