Choosing the Right Type

When you design a type, you decide which data your program can hold. Here is a chest type as many programmers would write it at first:

type Chest = {
    isLocked: boolean;
    keyName?: string; // only for locked chests
};

function openChest(chest: Chest): string {
    if (chest.isLocked) {
        return `You need the ${chest.keyName}`;
    }
    return "The chest opens";
}

console.log(openChest({ isLocked: false }));
console.log(openChest({ isLocked: true }));

This prints:

The chest opens
You need the undefined

The comment says that only locked chests have a key name, but the type doesn't say it. The type allows a locked chest without a key name, which nobody could ever open, and TypeScript has no objection when the program creates one. The optional property also makes the rest of the code clumsy: even after checking chest.isLocked, TypeScript only knows that chest.keyName is a string | undefined, because nothing in the type connects the two. In that branch, chest.keyName.length gives error TS18048: 'chest.keyName' is possibly 'undefined'.

A union describes the two states exactly:

type Chest =
    | { isLocked: false }
    | { isLocked: true; keyName: string };

true and false are literal types too, so a boolean property can be the tag of a discriminated union. With this type, the locked chest without a key name is an error:

main.ts(13,23): error TS2345: Argument of type '{ isLocked: true; }' is not assignable to parameter of type 'Chest'.
  Property 'keyName' is missing in type '{ isLocked: true; }' but required in type '{ isLocked: true; keyName: string; }'.

Inside if (chest.isLocked), TypeScript now knows that the chest has a keyName, a string, with no undefined. And an unlocked chest with a key name is an error too (TS2353).

Programmers call this “making impossible states impossible”: a good type allows only data that make sense. Watch out for these signs in your types:

  • a boolean with optional properties that make sense only when it's true, or only when it's false,
  • several optional properties that are always either all there, or all missing,
  • a status: string with comments about which properties go with which status.

Each of them is a union waiting to be written. When there are more than two states, use a text tag, such as status: "fighting" | "fleeing" | "defeated", instead of a boolean.

Instructions

The program prints undefined twice, because two monsters in the list can't exist. Replace the type Monster with a union of a fighting monster, { name: string; isDefeated: false; health: number }, and a defeated one, { name: string; isDefeated: true; defeatedBy: string }. TypeScript then reports the two monsters. Fix them: the troll is still fighting, with 40 health, and the skeleton was defeated by Bruno. The program then prints:

The wolf has 30 health
The goblin was defeated by Kira
The troll has 40 health
The skeleton was defeated by Bruno

Below the loop, write a function defeat with the parameters monster (a Monster) and hero (a text), and the return type Monster. It returns a new, defeated monster with the same name, defeated by hero. (Without the return type, TypeScript would infer isDefeated: boolean for the new object, and that doesn't fit Monster.) Print describeMonster(defeat(monsters[0], "Lena")). The program then prints:

The wolf has 30 health
The goblin was defeated by Kira
The troll has 40 health
The skeleton was defeated by Bruno
The wolf was defeated by Lena

Start programming for free

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

Or sign up with:

7/8