as const

A list of values and a literal union often describe the same thing. A bestiary, a book of monsters, lists every monster type, so the program has a list of them, while the rest of the program uses the type:

const monsterTypes = ["wolf", "troll", "dragon"];
type MonsterType = "wolf" | "troll" | "dragon";

const xpFor: Record<MonsterType, number> = { wolf: 15, troll: 40, dragon: 100 };

for (const monsterType of monsterTypes) {
    console.log(`${monsterType}: ${xpFor[monsterType]} XP`);
}
main.ts(7,36): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Record<MonsterType, number>'.
  No index signature with a parameter of type 'string' was found on type 'Record<MonsterType, number>'.

There are two problems. First, the monster types are written twice, and one day someone adds a new one to the list and forgets the type, or the other way round. Second, TypeScript doesn't connect the list with the type. For monsterTypes, it infers string[], an array of any texts, so an element of the list isn't a MonsterType, and reading xpFor[monsterType] is the error from the previous exercise.

Both problems are solved by as const after the array:

const monsterTypes = ["wolf", "troll", "dragon"] as const;

as const tells TypeScript to keep the exact values. The type of monsterTypes is now readonly ["wolf", "troll", "dragon"]: a read-only tuple (you know tuples from the Basic Types lesson) of exactly these three texts. Read-only means that the list can't change:

const monsterTypes = ["wolf", "troll", "dragon"] as const;
monsterTypes.push("griffin");
main.ts(2,14): error TS2339: Property 'push' does not exist on type 'readonly ["wolf", "troll", "dragon"]'.

That's what you want here: if the list could change while the program runs, no type could describe it in advance.

Now you can get the union from the list, so that the monster types are written only once:

type MonsterType = (typeof monsterTypes)[number];

Read it from the inside out. typeof monsterTypes in a type means “the type of the variable monsterTypes”. (This is TypeScript's typeof for types. JavaScript's typeof, which gives a text such as "string" when the program runs, is a different thing with the same name.) [number] then means “the type of an element at any index”: "wolf" | "troll" | "dragon". It looks complicated, but it's always the same recipe, (typeof list)[number], and the Utility Types lesson explains both parts in more detail.

With this, the bestiary works:

const monsterTypes = ["wolf", "troll", "dragon"] as const;
type MonsterType = (typeof monsterTypes)[number];

const xpFor: Record<MonsterType, number> = { wolf: 15, troll: 40, dragon: 100 };

for (const monsterType of monsterTypes) {
    console.log(`${monsterType}: ${xpFor[monsterType]} XP`);
}

This prints:

wolf: 15 XP
troll: 40 XP
dragon: 100 XP

And the list and the type can't get out of sync anymore. Add "griffin" to the list, and the type gets it too, so TypeScript reports every table that misses the new monster:

main.ts(4,7): error TS2741: Property 'griffin' is missing in type '{ wolf: number; troll: number; dragon: number; }' but required in type 'Record<"dragon" | "griffin" | "troll" | "wolf", number>'.

(In this message, TypeScript writes out the whole union instead of the name MonsterType.)

Typical mistake: forgetting as const. Without it, monsterTypes is a string[], and (typeof monsterTypes)[number] is just string. There's no error, but the type allows any text, even const monsterType: MonsterType = "potato";.

Instructions

Run the program: TypeScript can't read the tables with an element of classes. Add as const to the list, and replace the hand-written union in HeroClass with (typeof classes)[number]. The program then prints:

Choose your class:
- Warrior (120 health)
- Mage (70 health)
- Ranger (90 health)

A new class arrives: add "healer" to the end of the list. Run the program and add what TypeScript asks for: the class is called Healer, and a healer starts with 80 health. The program then prints:

Choose your class:
- Warrior (120 health)
- Mage (70 health)
- Ranger (90 health)
- Healer (80 health)

Start programming for free

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

Or sign up with:

5/8