Забытый case
Программы растут. Допустим, в игре появляются ключи: вы добавляете type Key в объединение Item, и теперь каждому switch по item.kind в программе нужен новый вариант. Найдёте ли вы их все в большой программе?
Искать не нужно: TypeScript найдёт их сам. Первый способ вы уже видели — в функции с возвращаемым типом, где каждая ветка что-то возвращает. Здесь у ключа ещё нет своего варианта:
type Potion = { kind: "potion"; name: string; heal: number };
type Weapon = { kind: "weapon"; name: string; damage: number };
type Key = { kind: "key"; name: string; opens: string };
type Item = Potion | Weapon | Key;
function describeItem(item: Item): string {
switch (item.kind) {
case "potion":
return `${item.name} (лечит ${item.heal})`;
case "weapon":
return `${item.name} (урон ${item.damage})`;
}
}main.ts(6,36): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.Для ключа ни один вариант не подходит, и функция дошла бы до конца, ничего не вернув. Это и сказано в сообщении. Оно полезно, но не говорит, какого вида не хватает. А в функции, которая ничего не возвращает (void), сообщения нет вообще: забытый ключ просто тихо ничего не делает.
Поэтому программисты на TypeScript добавляют проверку в ветку default. Она работает в любом switch и называет забытый вид по имени:
default:
const unhandled: never = item;
return unhandled;С этими тремя строками в конце switch выше TypeScript сообщит:
main.ts(13,19): error TS2322: Type 'Key' is not assignable to type 'never'.Как это работает? never — особый тип: тип значения, которого не может существовать. Ни одно значение в него не помещается; даже const unhandled: never = 5; — ошибка (Type '5' is not assignable to type 'never'.). Каждый case сужает предмет, поэтому в default остаются только те виды, у которых нет своего варианта. Когда у каждого вида есть свой вариант, не остаётся ничего: предмет там имеет тип never, и записать его в unhandled можно (а default никогда не выполняется). Если вы забыли какой-то вид, он остаётся, и в never не помещается. Сообщение называет забытый тип: Key.
А зачем return unhandled;? Благодаря ему каждая ветка функции что-то возвращает, и TypeScript не добавляет к сообщению ещё и ошибку TS2366 из примера выше. Значение типа never подходит под любой возвращаемый тип, так что эта строка работает и в функции void.
Такая проверка называется проверкой полноты (exhaustiveness check): TypeScript проверяет, что switch обрабатывает каждый вид. Пишите её в каждом switch по размеченному объединению. Тогда, когда однажды вы добавите новый вид, достаточно запустить программу — и TypeScript перечислит все switch, которым нужен новый вариант.
Типичные ошибки:
default, который возвращает что-то другое, напримерdefault: return "неизвестный предмет";. Для TypeScript забытые виды тогда обработаны, и ключ тихо превращается внеизвестный предмет.- Переменная без
: never.const unhandled = item;— обычная переменная, которая просто получает тип того, что осталось, так что сама строка ничего не проверяет. Именно: neverговорит: «здесь ничего не должно остаться».
Задание
- 1
Добавьте проверку полноты — ветку
defaultсnever— в обе функции: вdescribeItemи вuseItem. Программа по-прежнему выводит те же строки. - 2
Теперь в игре появляются свитки. Добавьте
type Scroll = { kind: "scroll"; name: string; spell: string };в объединениеItemи добавьте{ kind: "scroll", name: "Свиток света", spell: "Свет" }в конец инвентаря. Запустите программу и добавьте варианты, которых требует TypeScript:describeItemвозвращаетСвиток света (заклинание: Свет), аuseItemвыводитКира читает заклинание: Свет. Программа выведет:Зелье лечения (лечит 20) Серебряный меч (урон 12) Старый ключ (открывает склеп) Свиток света (заклинание: Свет) Кира пьёт: Зелье лечения Кира достаёт: Серебряный меч Кира открывает склеп Кира читает заклинание: Свет