Warm-up · Activity 1 of 7
// B3.5 · ~38 min · Beginner
never and exhaustiveness; build: an order-status state machine
After this lesson you can say where never appears, make a switch exhaustive and read what tsc reports for a new member, and build a state machine whose wrong moves do not compile.
Lesson 5 of 5 in B3 Narrowing
You will be able to
- Explain where never appears, what is assignable to it, and functions that never return
- Make a switch exhaustive with a never check or assertNever, and read the error for a new member
- Model a state machine as a discriminated union whose transition functions accept only valid states
Predict · Activity 2 of 7
Predict before you read on. label has no case for "cancelled" and no default. What does tsc say?
type Pending = { status: "pending"; id: number }; type Paid = { status: "paid"; id: number; paidAt: string }; type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string }; type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string }; type Cancelled = { status: "cancelled"; id: number; reason: string }; type Order = Pending | Paid | Shipped | Delivered | Cancelled; function label(order: Order): string { switch (order.status) { case "pending": return "waiting"; case "paid": return "paid"; case "shipped": return "on its way"; case "delivered": return "delivered"; } }Practice · Activity 3 of 7
Fill in the parameter type, so that assertNever(order) compiles only when every status has a case.
function assertNever(value: ____): never { throw new Error("Unexpected order: " + JSON.stringify(value)); }function assertNever(value: ): never {Practice · Activity 4 of 7
With the order states below, match each expression to its type.
type Pending = { status: "pending"; id: number }; type Paid = { status: "paid"; id: number; paidAt: string }; type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string }; type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string }; type Cancelled = { status: "cancelled"; id: number; reason: string }; type Order = Pending | Paid | Shipped | Delivered | Cancelled;Practice · Activity 5 of 7
A new status, Returned, was added to Order (line 6). Which lines does tsc reject?
type Pending = { status: "pending"; id: number }; type Paid = { status: "paid"; id: number; paidAt: string }; type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string }; type Cancelled = { status: "cancelled"; id: number; reason: string }; type Returned = { status: "returned"; id: number; refund: number }; type Order = Pending | Paid | Shipped | Cancelled | Returned; function assertNever(value: never): never { throw new Error("Unexpected order: " + JSON.stringify(value)); } function isOpen(order: Order): boolean { switch (order.status) { case "pending": case "paid": case "shipped": return true; case "cancelled": return false; default: return assertNever(order); } } function canCancel(order: Order) { switch (order.status) { case "pending": case "paid": return true; case "shipped": case "cancelled": return false; default: { const unhandled: never = order; return unhandled; } } } function icon(order: Order) { switch (order.status) { case "pending": return "P"; case "paid": return "€"; case "shipped": return "S"; case "cancelled": return "X"; } }Brain teaser · Activity 6 of 7
Brain teaser. fail is an arrow function with the return type never. What does tsc report?
const fail = (msg: string): never => { throw new Error(msg); }; function shout(x: string | undefined): string { if (x === undefined) fail("missing"); return x.toUpperCase(); }Apply · Activity 7 of 7
Mini-task. A document goes from draft to review to published, and a reviewer can send it back to draft. Model the three stages as a discriminated union, write the transitions submit, reject and publish so that each accepts only the stage it starts from, and write an exhaustive nextAction(doc) with assertNever in default. Then add a fourth stage, archived, and follow tsc's errors until it compiles again. Check it in the editor, or with npx tsc and node main.ts.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A support ticket that only moves forward
A ticket is open, assigned or resolved, and each state carries the data it has by then. assign, resolve and reopen accept only the state they start from, so assign(done, "Leo") would not compile. describe ends with assertNever, which is also the safety net for data that tsc never saw: the JSON from the "server" has a state no case handles. Run npx tsc, then node main.ts. Then add type Escalated = { state: "escalated"; id: number; title: string } to Ticket and run tsc again.
main.ts
// A support ticket: open -> assigned -> resolved, and a resolved ticket can be reopened.
type Open = { state: "open"; id: number; title: string };
type Assigned = { state: "assigned"; id: number; title: string; agent: string };
type Resolved = { state: "resolved"; id: number; title: string; agent: string; note: string };
type Ticket = Open | Assigned | Resolved;
function assertNever(value: never): never {
throw new Error("Unexpected ticket: " + JSON.stringify(value));
}
// Each transition accepts only the state it may start from.
function assign(ticket: Open, agent: string): Assigned {
return { ...ticket, state: "assigned", agent };
}
function resolve(ticket: Assigned, note: string): Resolved {
return { ...ticket, state: "resolved", note };
}
function reopen(ticket: Resolved): Open {
return { state: "open", id: ticket.id, title: ticket.title };
}
function describe(ticket: Ticket): string {
const head = "#" + ticket.id + " " + ticket.title + ": ";
switch (ticket.state) {
case "open":
return head + "waiting for an agent";
case "assigned":
return head + ticket.agent + " is on it";
case "resolved":
return head + "resolved by " + ticket.agent + " (" + ticket.note + ")";
default:
return assertNever(ticket); // compiles only while every state has a case
}
}
const opened: Open = { state: "open", id: 7, title: "Login fails" };
const taken = assign(opened, "Mia");
const done = resolve(taken, "password reset");
for (const ticket of [opened, taken, done, reopen(done)]) {
console.log(describe(ticket));
}
// tsc cannot check data that arrives while the program runs:
const fromServer: Ticket = JSON.parse('{"state":"escalated","id":8,"title":"Disk full"}');
try {
console.log(describe(fromServer));
} catch (error) {
console.log(error instanceof Error ? error.message : "unknown error");
}
Run it with
npx tsc
node main.tsOutput
#7 Login fails: waiting for an agent
#7 Login fails: Mia is on it
#7 Login fails: resolved by Mia (password reset)
#7 Login fails: waiting for an agent
Unexpected ticket: {"state":"escalated","id":8,"title":"Disk full"}- With Escalated in Ticket, tsc reports the assertNever line: Argument of type 'Escalated' is not assignable to parameter of type 'never'.
- JSON.parse returns any, so tsc accepts it as a Ticket without checking; assertNever catches the unknown state while the program runs.
- reopen builds a new Open object instead of spreading, so agent and note do not travel back into the open ticket.
- The spread { ...ticket, state: "assigned", agent } keeps id and title and replaces state.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 3
Handle the status tsc names
badge has a never check in default, and tsc reports error TS2322: Type 'Cancelled' is not assignable to type 'never'. Add the missing case, so that a cancelled order gives "CANCELLED (" followed by its reason and ")". Keep the never check.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
The diagnostic names the member that reaches default: Cancelled. Which case is missing?
Hint 2
Inside case "cancelled", order is a Cancelled, so order.reason compiles.
Hint 3
Before default, add: case "cancelled": return "CANCELLED (" + order.reason + ")";
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function badge(order: Order): string {
switch (order.status) {
case "pending":
return "PENDING";
case "paid":
return "PAID";
case "shipped":
return "SHIPPED " + order.tracking;
case "delivered":
return "DELIVERED";
case "cancelled":
return "CANCELLED (" + order.reason + ")";
default: {
const unhandled: never = order;
return unhandled;
}
}
}
Run it on your computer
Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.ts
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function badge(order: Order): string {
switch (order.status) {
case "pending":
return "PENDING";
case "paid":
return "PAID";
case "shipped":
return "SHIPPED " + order.tracking;
case "delivered":
return "DELIVERED";
default: {
const unhandled: never = order;
return unhandled;
}
}
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {badge} from './main.ts';
test('a cancelled order shows its reason', () => {
const text = badge({status: 'cancelled', id: 4, reason: 'out of stock'});
assert.equal(text, 'CANCELLED (out of stock)', `badge of a cancelled order gave ${JSON.stringify(text)}`);
});
test('the other statuses keep their badges', () => {
assert.equal(badge({status: 'pending', id: 1}), 'PENDING', 'badge of a pending order should be "PENDING"');
assert.equal(badge({status: 'shipped', id: 2, paidAt: '2026-10-04', tracking: 'DHL-7'}), 'SHIPPED DHL-7', 'badge of a shipped order should be "SHIPPED DHL-7"');
});
package.json
{
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"lib": [
"esnext",
"dom"
],
"types": [],
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.
Run the program:
npx tsc
node main.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 2 of 3
Make assertNever throw
nextStep is exhaustive, and tsc accepts the file. But assertNever just returns its argument, so an order with a status from outside the program, such as "lost" from JSON, slips through as an object. Make assertNever throw an Error whose message is "Unexpected order: " followed by JSON.stringify(value).
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
A function with the return type never must not return normally. return value only compiles because value is itself never.
Hint 2
You cannot read value.status: the type never has no properties. JSON.stringify(value) works for any value.
Hint 3
throw new Error("Unexpected order: " + JSON.stringify(value));
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
throw new Error("Unexpected order: " + JSON.stringify(value));
}
export function nextStep(order: Order): string {
switch (order.status) {
case "pending":
return "pay";
case "paid":
return "ship";
case "shipped":
return "deliver";
case "delivered":
case "cancelled":
return "none";
default:
return assertNever(order);
}
}
Run it on your computer
Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.ts
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
return value;
}
export function nextStep(order: Order): string {
switch (order.status) {
case "pending":
return "pay";
case "paid":
return "ship";
case "shipped":
return "deliver";
case "delivered":
case "cancelled":
return "none";
default:
return assertNever(order);
}
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {nextStep} from './main.ts';
test('every known status has a next step', () => {
assert.equal(nextStep({status: 'pending', id: 1}), 'pay', 'nextStep of a pending order should be "pay"');
assert.equal(nextStep({status: 'paid', id: 1, paidAt: '2026-10-04'}), 'ship', 'nextStep of a paid order should be "ship"');
assert.equal(nextStep({status: 'cancelled', id: 1, reason: 'late'}), 'none', 'nextStep of a cancelled order should be "none"');
});
test('an unknown status from JSON throws with the order in the message', () => {
assert.throws(() => nextStep(JSON.parse('{"status":"lost","id":9}')), {message: 'Unexpected order: {"status":"lost","id":9}'}, 'nextStep should throw an Error with the message "Unexpected order: " plus the order as JSON');
});
package.json
{
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"lib": [
"esnext",
"dom"
],
"types": [],
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.
Run the program:
npx tsc
node main.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 3 of 3
Build: an order-status state machine
Build the state machine for orders. Step 1: give pay, ship, deliver and cancel the narrowest parameter types, so that only valid moves compile: pay starts from pending, ship from paid, deliver from shipped, and cancel from pending or paid. The three // @ts-expect-error lines in forbiddenMoves check this: tsc reports them while a wrong move still compiles. Step 2: write describe, with one case per status and default: return assertNever(order). The tests show the exact texts.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Start with tsc's errors. With order: Order, order.paidAt does not exist on every member, and ship(pending, …) compiles, so its @ts-expect-error is unused.
Hint 2
The parameter type is the state a move starts from: pay(order: Pending, …), ship(order: Paid, …), deliver(order: Shipped, …). cancel takes a union of two states.
Hint 3
In describe, switch (order.status) with five cases; in case "paid" you may read order.paidAt, in case "cancelled" order.reason.
Hint 4
Each case returns "Order " + order.id + ": " followed by its text, e.g. "shipped, tracking " + order.tracking. default: return assertNever(order);
Show a solution
One way to solve it. Yours can look different and still pass the checks.
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
throw new Error("Unexpected order: " + JSON.stringify(value));
}
// Step 1: give each transition the narrowest parameter type.
export function pay(order: Pending, paidAt: string): Paid {
return { status: "paid", id: order.id, paidAt };
}
export function ship(order: Paid, tracking: string): Shipped {
return { status: "shipped", id: order.id, paidAt: order.paidAt, tracking };
}
export function deliver(order: Shipped, deliveredAt: string): Delivered {
return { status: "delivered", id: order.id, paidAt: order.paidAt, tracking: order.tracking, deliveredAt };
}
export function cancel(order: Pending | Paid, reason: string): Cancelled {
return { status: "cancelled", id: order.id, reason };
}
// Step 2: describe every status, with assertNever in default.
export function describe(order: Order): string {
const head = "Order " + order.id + ": ";
switch (order.status) {
case "pending":
return head + "waiting for payment";
case "paid":
return head + "paid on " + order.paidAt;
case "shipped":
return head + "shipped, tracking " + order.tracking;
case "delivered":
return head + "delivered on " + order.deliveredAt;
case "cancelled":
return head + "cancelled (" + order.reason + ")";
default:
return assertNever(order);
}
}
// These moves must not compile. Keep them exactly as they are.
export function forbiddenMoves(pending: Pending, shipped: Shipped, delivered: Delivered): void {
// @ts-expect-error: an order must be paid before it is shipped
ship(pending, "DHL-1");
// @ts-expect-error: a shipped order can no longer be cancelled
cancel(shipped, "changed my mind");
// @ts-expect-error: a delivered order cannot be delivered again
deliver(delivered, "2026-10-09");
}
Run it on your computer
Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.ts
export type Pending = { status: "pending"; id: number };
export type Paid = { status: "paid"; id: number; paidAt: string };
export type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
export type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
export type Cancelled = { status: "cancelled"; id: number; reason: string };
export type Order = Pending | Paid | Shipped | Delivered | Cancelled;
export function assertNever(value: never): never {
throw new Error("Unexpected order: " + JSON.stringify(value));
}
// Step 1: give each transition the narrowest parameter type.
export function pay(order: Order, paidAt: string): Paid {
return { status: "paid", id: order.id, paidAt };
}
export function ship(order: Order, tracking: string): Shipped {
return { status: "shipped", id: order.id, paidAt: order.paidAt, tracking };
}
export function deliver(order: Order, deliveredAt: string): Delivered {
return { status: "delivered", id: order.id, paidAt: order.paidAt, tracking: order.tracking, deliveredAt };
}
export function cancel(order: Order, reason: string): Cancelled {
return { status: "cancelled", id: order.id, reason };
}
// Step 2: describe every status, with assertNever in default.
export function describe(order: Order): string {
throw new Error("describe is not written yet");
}
// These moves must not compile. Keep them exactly as they are.
export function forbiddenMoves(pending: Pending, shipped: Shipped, delivered: Delivered): void {
// @ts-expect-error: an order must be paid before it is shipped
ship(pending, "DHL-1");
// @ts-expect-error: a shipped order can no longer be cancelled
cancel(shipped, "changed my mind");
// @ts-expect-error: a delivered order cannot be delivered again
deliver(delivered, "2026-10-09");
}
main.test.ts
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {pay, ship, deliver, cancel, describe} from './main.ts';
const pending = {status: 'pending', id: 1};
const paid = {status: 'paid', id: 1, paidAt: '2026-10-04'};
const shipped = {status: 'shipped', id: 1, paidAt: '2026-10-04', tracking: 'DHL-1'};
const delivered = {status: 'delivered', id: 1, paidAt: '2026-10-04', tracking: 'DHL-1', deliveredAt: '2026-10-06'};
test('pay turns a pending order into a paid one', () => {
assert.deepEqual(pay(pending, '2026-10-04'), paid, 'pay(pending, "2026-10-04") should give the paid order');
});
test('ship keeps the payment date and adds the tracking number', () => {
assert.deepEqual(ship(paid, 'DHL-1'), shipped, 'ship(paid, "DHL-1") should give the shipped order');
});
test('deliver keeps the tracking number and adds the delivery date', () => {
assert.deepEqual(deliver(shipped, '2026-10-06'), delivered, 'deliver(shipped, "2026-10-06") should give the delivered order');
});
test('cancel gives a cancelled order with the reason', () => {
assert.deepEqual(cancel(paid, 'out of stock'), {status: 'cancelled', id: 1, reason: 'out of stock'}, 'cancel(paid, "out of stock") should give a cancelled order');
});
test('describe words every status', () => {
assert.equal(describe(pending), 'Order 1: waiting for payment', 'describe of the pending order is wrong');
assert.equal(describe(paid), 'Order 1: paid on 2026-10-04', 'describe of the paid order is wrong');
assert.equal(describe(shipped), 'Order 1: shipped, tracking DHL-1', 'describe of the shipped order is wrong');
assert.equal(describe(delivered), 'Order 1: delivered on 2026-10-06', 'describe of the delivered order is wrong');
assert.equal(describe({status: 'cancelled', id: 2, reason: 'out of stock'}), 'Order 2: cancelled (out of stock)', 'describe of the cancelled order is wrong');
});
test('describe throws for a status it does not know', () => {
assert.throws(() => describe(JSON.parse('{"status":"lost","id":9}')), /Unexpected order/, 'describe should throw "Unexpected order: ..." for status "lost"');
});
package.json
{
"type": "module"
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"lib": [
"esnext",
"dom"
],
"types": [],
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true
},
"include": [
"**/*.ts"
],
"exclude": [
"**/*.test.ts"
]
}
npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.
Run the program:
npx tsc
node main.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsCommon mistakes
A never function that can return
function fail(message: string): never {
console.log("Error: " + message);
}
fail("payment declined");
What tsc or Node.js prints
main.ts(1,33): error TS2534: A function returning 'never' cannot have a reachable end point.Why, and the fix
The return type never promises that the function does not come back, so code after a call to it may count as unreachable. This body logs and then ends normally. Throw instead: throw new Error(message);. If the function really should return, give it the return type void.
An exhaustive switch without a never check
type Pending = { status: "pending"; id: number };
type Paid = { status: "paid"; id: number; paidAt: string };
type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
type Cancelled = { status: "cancelled"; id: number; reason: string };
type Order = Pending | Paid | Shipped | Delivered | Cancelled;
function label(order: Order): string {
switch (order.status) {
case "pending":
return "waiting";
case "paid":
return "paid";
case "shipped":
return "on its way";
case "delivered":
return "delivered";
}
}
console.log(label({ status: "pending", id: 1 }));
What tsc or Node.js prints
main.ts(8,31): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.Why, and the fix
A cancelled order falls out of the switch, and the declared return type string does not allow that. tsc finds the gap but does not name it. Add the case, and add default: return assertNever(order); (or const unhandled: never = order) so that the next missing status is reported by name, at the place that must handle it.
A transition that accepts every state
type Pending = { status: "pending"; id: number };
type Paid = { status: "paid"; id: number; paidAt: string };
type Shipped = { status: "shipped"; id: number; paidAt: string; tracking: string };
type Delivered = { status: "delivered"; id: number; paidAt: string; tracking: string; deliveredAt: string };
type Cancelled = { status: "cancelled"; id: number; reason: string };
type Order = Pending | Paid | Shipped | Delivered | Cancelled;
function cancel(order: Order, reason: string): Cancelled {
return { status: "cancelled", id: order.id, reason };
}
function forbiddenMoves(shipped: Shipped): void {
// @ts-expect-error: a shipped order can no longer be cancelled
cancel(shipped, "changed my mind");
}
What tsc or Node.js prints
main.ts(13,3): error TS2578: Unused '@ts-expect-error' directive.Why, and the fix
The directive says the next line must be a compile error, and it is not: cancel takes any Order, so cancelling a shipped order compiles. Narrow the parameter to the states a cancellation may start from: function cancel(order: Pending | Paid, reason: string): Cancelled. Then the call is an error, the directive uses it up, and tsc is quiet.
TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.