Размеченные объединения
В конце прошлого урока вы отличали зелья от оружия по их свойствам: "heal" in item. Это работает, но ненадёжно: TypeScript не заметит опечатку вроде "heel" in item, а если однажды свойство heal появится у предмета другого вида, проверка тихо перестанет работать.
Обычный способ описать в TypeScript «один из нескольких видов вещей» — дать каждому объекту свойство, которое говорит, что это такое. Его обычно называют kind (вид), и в каждом типе объединения у него свой литеральный тип:
type Potion = { kind: "potion"; name: string; heal: number };
type Weapon = { kind: "weapon"; name: string; damage: number };
type Item = Potion | Weapon;
const inventory: Item[] = [
{ kind: "potion", name: "Зелье лечения", heal: 20 },
{ kind: "weapon", name: "Серебряный меч", damage: 12 },
];Такое объединение называется размеченным объединением (discriminated union): у всех его типов есть общее свойство kind, и по его значению типы различают. Это свойство ещё называют тегом. Проверка тега сужает тип:
function describeItem(item: Item): string {
if (item.kind === "potion") {
return `${item.name} (лечит ${item.heal})`;
} else {
return `${item.name} (урон ${item.damage})`;
}
}
for (const item of inventory) {
console.log(describeItem(item));
}Программа выведет:
Зелье лечения (лечит 20)
Серебряный меч (урон 12)Внутри if предмет — это Potion, ведь только у зелий kind: "potion"; в ветке else это Weapon. Свойства, которые есть у каждого типа объединения (здесь это kind и name), можно читать без проверки.
Когда программа работает, тег — обычное свойство: console.log(inventory[1]) выведет { kind: "weapon", name: "Серебряный меч", damage: 12 }. Но для TypeScript он — ключ ко всему объекту. TypeScript знает, какие свойства подходят к каждому виду, поэтому проверяет и ваши данные:
type Potion = { kind: "potion"; name: string; heal: number };
type Weapon = { kind: "weapon"; name: string; damage: number };
type Item = Potion | Weapon;
const loot: Item[] = [
{ kind: "potion", name: "Настой", damage: 5 },
{ kind: "poton", name: "Тоник", heal: 5 },
];main.ts(6,39): error TS2353: Object literal may only specify known properties, and 'damage' does not exist in type 'Potion'.
main.ts(7,7): error TS2820: Type '"poton"' is not assignable to type '"potion" | "weapon"'. Did you mean '"potion"'?Зелье с уроном TypeScript заметит, ведь у Potion нет свойства damage. Заметит он и опечатку в виде, да ещё и подскажет правильное слово. Опечатка в сравнении, например item.kind === "poton", даёт уже знакомую вам ошибку TS2367: у типов нет пересечения.
Типичные ошибки:
- Объект без тега.
const tonic: Item = { name: "Тоник", heal: 5 };даётerror TS2322: Type '{ name: string; heal: number; }' is not assignable to type 'Item'., а вторая строка сообщения объясняет:Property 'kind' is missing in type '{ name: string; heal: number; }' but required in type 'Potion'. kind: stringв типах вместо литерального типа. Тогдаkindможет быть любым текстом, типы он не различает, и проверка ничего не сужает: послеif (item.kind === "potion")чтениеitem.healпо-прежнему остаётся ошибкой.
Задание
- 1
Наградой за квест бывает золото или предмет. В списке из редактора уже есть оба вида, но TypeScript пока не знает про
ItemReward. Опишите его для наград-предметов из списка:kind: "item",name(текст) иcount(число). - 2
Напишите функцию
describeRewardс параметромrewardтипаReward. Для золота она возвращает, например,Золото: 50, для предмета — название и количество, напримерЗелье лечения ×3. Выведите все награды из списка. Программа выведет:Золото: 50 Серебряный меч ×1 Зелье лечения ×3 Золото: 30 - 3
Сложите золото из всех наград-золота и выведите сумму после наград:
Золото: 50 Серебряный меч ×1 Зелье лечения ×3 Золото: 30 Итого золота: 80