Типы индексированного доступа
В уроке про обобщения (generics) вы писали Hero[K] — тип свойства K у Hero. Ключ в квадратных скобках можно записать и сразу, как литеральный тип в кавычках. Hero["heroClass"] — это тип свойства heroClass у Hero. Такая запись называется типом индексированного доступа (indexed access type):
interface Hero {
name: string;
heroClass: "warrior" | "mage" | "ranger";
level: number;
}
function trainer(heroClass: Hero["heroClass"]): string {
if (heroClass === "warrior") {
return "Бруно, мастер оружия";
} else if (heroClass === "mage") {
return "Эма, верховная чародейка";
}
return "Том, старый охотник";
}
console.log(trainer("mage"));
console.log(trainer("healer"));main.ts(17,21): error TS2345: Argument of type '"healer"' is not assignable to parameter of type '"mage" | "ranger" | "warrior"'.Параметр принимает ровно те классы, что есть в Hero, и TypeScript выписывает их в сообщении. Почему бы не записать объединение ещё раз? Потому что тогда классы окажутся в двух местах. С Hero["heroClass"] они записаны один раз, в Hero. Когда там появится новый класс, trainer примет и его, и никому не нужно помнить, что надо что-то поменять.
Для объединения объектных типов, как в уроке про моделирование данных, вы получаете объединение типов этого свойства во всех вариантах. У каждого квеста есть status, поэтому Quest["status"] — объединение всех статусов:
type Quest =
| { status: "open"; title: string }
| { status: "done"; title: string; reward: number }
| { status: "failed"; title: string; reason: string };
type QuestStatus = Quest["status"]; // "open" | "done" | "failed"Так работают только свойства, которые есть у каждого варианта: Quest["reward"] даёт Property 'reward' does not exist on type 'Quest'.
Вместе с Record из урока про моделирование данных получается объект с одним значением для каждого статуса: у Record<Quest["status"], number> есть ключи open, done и failed. Он пригодится вам в итоговом проекте.
Ключом может быть и тип number. В типе массива [number] означает «элемент с любым индексом»: при type Names = string[]; тип Names[number] — это string. Это вторая половина рецепта (typeof classes)[number] из урока про моделирование данных: typeof classes — тип списка, а [number] — тип его элементов. Первую половину, typeof, мы разберём в следующем упражнении.
Типичные ошибки:
- Точка вместо скобок. В типах
Hero.heroClassне работает, но сообщение подсказывает, что писать:main.ts(7,29): error TS2713: Cannot access 'Hero.heroClass' because 'Hero' is a type, but not a namespace. Did you mean to retrieve the type of the property 'heroClass' in 'Hero' with 'Hero["heroClass"]'? - Ключ без кавычек,
Hero[heroClass]. Ключ — это тип, поэтому он записывается литералом в кавычках. Без них TypeScript читаетheroClassкак имя. Вtrainerэто имя параметра, то есть значения, поэтому вы получите:
Там, где ничего не называетсяmain.ts(7,34): error TS2538: Type 'heroClass' cannot be used as an index type. main.ts(7,34): error TS2749: 'heroClass' refers to a value, but is being used as a type here. Did you mean 'typeof heroClass'?heroClass, вместо сообщения TS2749 вы получитеCannot find name 'heroClass'. - Ключ, которого в типе нет,
Hero["class"]:Property 'class' does not exist on type 'Hero'.
Задание
- 1
Измените тип параметра
printRarityсоstringнаItem["rarity"]и запустите программу. TypeScript найдёт опечатку в одном из вызовов. Исправьте её, и программа выведет и легендарные предметы. - 2
Под вызовами посчитайте предметы каждой редкости. Создайте константу
countsтипаRecord<Item["rarity"], number>со значением0для каждой редкости. В циклеfor...ofпоshopприбавляйте единицу кcounts[item.rarity]для каждого предмета, а после цикла выведитеcounts:{ common: 2, rare: 1, legendary: 1 } - 3
В магазине появилась новая редкость,
"epic". Добавьте её в объединение вItem, добавьте предмет{ name: "Чешуя дракона", rarity: "epic", price: 150 }в конецshopи вызовитеprintRarity("epic")под двумя другими вызовами. TypeScript сообщит о месте, которое должно знать о новой редкости; исправьте его. ВprintRarityменять ничего не нужно. Программа заканчивается так:legendary: - Перо феникса (цена: 300) epic: - Чешуя дракона (цена: 150) { common: 2, rare: 1, legendary: 1, epic: 1 }