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 undefinedThe 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
booleanwith optional properties that make sense only when it'strue, or only when it'sfalse, - several optional properties that are always either all there, or all missing,
- a
status: stringwith 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 BrunoBelow 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 LenaStart programming for free
7/8