Quest States

A discriminated union is good not only for different kinds of things, but also for the different states of one thing. A quest in Kira's log can be open, done, or failed, and each state has its own data: a done quest has a reward, a failed one has the reason why it failed, and an open one has neither:

type Quest =
    | { status: "open"; title: string }
    | { status: "done"; title: string; reward: number }
    | { status: "failed"; title: string; reason: string };

The tag here is called status, because that's what it describes. The name is up to you; it just has to be the same property in every member of the union, with a different literal type in each. A union this long is easier to read on several lines. Then you can write a | in front of each member, the first one too: TypeScript ignores a bar at the very beginning.

The type says exactly what you know about each quest. Only a done quest has a reward, so reading quest.reward without a check is an error:

type Quest =
    | { status: "open"; title: string }
    | { status: "done"; title: string; reward: number }
    | { status: "failed"; title: string; reason: string };

const quests: Quest[] = [
    { status: "done", title: "Wolves in the Woods", reward: 50 },
    { status: "open", title: "The Lost Crown" },
    { status: "failed", title: "Save the Miller", reason: "the mill burned down" },
];

for (const quest of quests) {
    console.log(`${quest.title}: ${quest.reward} gold`);
}
main.ts(13,42): error TS2339: Property 'reward' does not exist on type 'Quest'.
  Property 'reward' does not exist on type '{ status: "open"; title: string; }'.

The second line of the message even says which quest has no reward: the open one. And a quest that makes no sense can't be written at all: const lostCrown: Quest = { status: "open", title: "The Lost Crown", reward: 100 }; gives error TS2353: Object literal may only specify known properties, and 'reward' does not exist in type '{ status: "open"; title: string; }'.

The property title is in all three members, so you can read it on any quest. For the rest, check the status first:

for (const quest of quests) {
    if (quest.status === "done") {
        console.log(`${quest.title}: ${quest.reward} gold`);
    } else {
        console.log(`${quest.title}: no reward`);
    }
}

This prints:

Wolves in the Woods: 50 gold
The Lost Crown: no reward
Save the Miller: no reward

Compare this with a single type with optional properties, { status: string; title: string; reward?: number; reason?: string }. It allows an open quest with a reward, a done quest without one, and even a status "dnoe". The next exercise is about such types.

Instructions

Run the program: the third quest doesn't fit the type. Add a third member to the union Quest: a failed quest, with status: "failed", a title and a reason (both texts).

Write a function describeQuest with a parameter quest of the type Quest that returns a description of the quest. Use switch on quest.status (with an exhaustiveness check in default, for practice), and print the description of every quest. The program then prints:

Wolves in the Woods: done, +50 gold
The Lost Crown: open
Save the Miller: failed (the mill burned down)
The Goblin Bridge: done, +30 gold

Add up the rewards of the done quests, and print the total at the end:

Wolves in the Woods: done, +50 gold
The Lost Crown: open
Save the Miller: failed (the mill burned down)
The Goblin Bridge: done, +30 gold
Total reward: 80 gold

Start programming for free

By signing up, you agree to the Terms of Service and Privacy Policy.

Or sign up with:

6/8