as const
Список значений и литеральное объединение часто описывают одно и то же. Бестиарий — книга монстров — перечисляет все типы монстров, поэтому в программе есть их список, а остальная часть программы использует тип:
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>'.Здесь две проблемы. Во-первых, типы монстров записаны дважды, и однажды кто-нибудь добавит новый в список и забудет про тип — или наоборот. Во-вторых, TypeScript не связывает список с типом. Для monsterTypes он выводит string[] — массив любых текстов, поэтому элемент списка не является MonsterType, и чтение xpFor[monsterType] — это ошибка из прошлого упражнения.
Обе проблемы решает as const после массива:
const monsterTypes = ["wolf", "troll", "dragon"] as const;as const велит TypeScript сохранить точные значения. Теперь тип monsterTypes — readonly ["wolf", "troll", "dragon"]: кортеж только для чтения (кортежи вы знаете из урока «Основные типы») ровно из этих трёх текстов. «Только для чтения» значит, что список нельзя изменить:
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"]'.Здесь это как раз то, что нужно: если бы список мог меняться во время работы программы, никакой тип не смог бы описать его заранее.
Теперь объединение можно получить из списка, и типы монстров будут записаны только один раз:
type MonsterType = (typeof monsterTypes)[number];Читайте изнутри наружу. typeof monsterTypes в типе означает «тип переменной monsterTypes». (Это typeof из TypeScript для типов. typeof из JavaScript, который во время работы программы даёт текст вроде "string", — другая вещь с тем же названием.) [number] означает «тип элемента с любым индексом»: "wolf" | "troll" | "dragon". Выглядит сложно, но это всегда один и тот же рецепт, (typeof list)[number], и обе его части подробнее объясняются в уроке «Вспомогательные типы».
Так бестиарий работает:
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`);
}Программа выведет:
wolf: 15 XP
troll: 40 XP
dragon: 100 XPИ список с типом больше не разойдутся. Добавьте в список "griffin" — тип получит его тоже, и TypeScript укажет на каждую таблицу, где нового монстра нет:
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>'.(В этом сообщении TypeScript выписывает всё объединение вместо имени MonsterType.)
Типичная ошибка — забыть as const. Без него monsterTypes — это string[], а (typeof monsterTypes)[number] — просто string. Ошибки нет, но тип допускает любой текст, даже const monsterType: MonsterType = "картошка";.
Задание
- 1
Запустите программу: TypeScript не может прочитать таблицы по элементу
classes. Добавьте к спискуas constи замените объединение, написанное вручную вHeroClass, на(typeof classes)[number]. Программа выведет:Выберите класс: - Воин (здоровье: 120) - Маг (здоровье: 70) - Следопыт (здоровье: 90) - 2
Появляется новый класс: добавьте
"healer"в конец списка. Запустите программу и добавьте то, чего требует TypeScript: класс называетсяЦелитель, а здоровье целителя в начале —80. Программа выведет:Выберите класс: - Воин (здоровье: 120) - Маг (здоровье: 70) - Следопыт (здоровье: 90) - Целитель (здоровье: 80)