Проверка полноты через never
Однажды на сортировочную ленту приезжают посылки нового вида — длинные тубусы. Для конвертов и для банок есть свои жёлоба, а для тубусов нет. Тубус проезжает мимо всех жёлобов и падает на пол в конце ленты, и никто этого не замечает.
В программе так бывает, когда в объединение добавляют вариант. Фигур стало три, а switch в функции label по-прежнему разбирает две:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number }
| { kind: "triangle"; side: number };
function label(s: Shape) {
switch (s.kind) {
case "circle":
return "circle, r = " + s.r;
case "square":
return "square, side = " + s.side;
}
}
console.log(label({ kind: "triangle", side: 2 })); // undefinedtsc молчит. Треугольник проходит мимо обоих case, функция доходит до конца, и программа печатает undefined. Для функции без аннотации результата это законно: tsc выводит для неё тип string | undefined. В большом проекте такой switch не один, и найти вручную все места, где забыт новый вариант, трудно.
Что остаётся в default
Вспомните тип never: так обозначают значение, которого не бывает. В switch по признаку каждый case убирает свой вариант из типа s. Если разобраны все варианты, в ветку default не может попасть ни одна фигура, и у s там тип never. На этом построена проверка полноты: компилятор убеждается, что switch разобрал каждый вариант объединения:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number };
function label(s: Shape) {
switch (s.kind) {
case "circle":
return "circle, r = " + s.r;
case "square":
return "square, side = " + s.side;
default:
const rest: never = s;
return rest;
}
}Давайте разберём пример по строкам:
case "circle"иcase "square"разбирают оба варианта объединения.- В
defaultуsтипnever: после двухcaseвариантов не осталось. const rest: never = s;компилируется: значение типаneverподходит к переменной типаnever.return restдаёт функции ответ и на этом пути. Пока все варианты разобраны, программа до этой строки не дойдёт.
Строка с never — контрольный жёлоб в конце ленты. Пока для каждого вида посылок есть свой жёлоб, в контрольный ничего не падает.
Так же работает switch прямо по объединению литералов. Например, если у p тип "card" | "cash", каждый case убирает из типа p свой литерал, и в default остаётся never.
Забытый вариант
Теперь добавьте в Shape третий вариант { kind: "triangle"; side: number } и ничего не меняйте в label. В default теперь попадает треугольник, а треугольник к типу never не подходит:
error TS2322: Type '{ kind: "triangle"; side: number; }' is not assignable to type 'never'.tsc показывает строку с never и называет вариант, который забыли разобрать. Добавьте case "triangle", и ошибка исчезнет. Так компилятор найдёт каждый switch в проекте, где нужен новый case.
Здесь аналогия с лентой перестаёт работать. На почте сигнал прозвучит, только когда тубус уже упадёт в контрольный жёлоб. tsc сообщает о забытом варианте раньше — при проверке кода, когда программа ещё ни разу не запускалась.
У функции с аннотацией результата есть и вторая подсказка. Если написать function label(s: Shape): string без default, tsc заметит путь без return:
error TS2366: Function lacks ending return statement and return type does not include 'undefined'.Но вариант в этом сообщении не назван, а функция, которая ничего не возвращает, такой ошибки не получит вовсе. Проверка через never работает в любой функции.
Этот вопрос часто задают на собеседовании: как сделать, чтобы компилятор сам нашёл необработанный вариант объединения. Ответ: в default присвоить значение переменной типа never.
Частая ошибка
В default вернули запасной ответ:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number }
| { kind: "triangle"; side: number };
function label(s: Shape) {
switch (s.kind) {
case "circle":
return "circle, r = " + s.r;
case "square":
return "square, side = " + s.side;
default:
return "some shape"; // ✗
}
}tsc молчит: "some shape" — законная строка, и в default можно положить что угодно. Треугольник получит запасной ответ, и о новом варианте никто не узнает. Такой default ловит все неразобранные варианты, в том числе забытые. Оставьте в default проверку через never, а для каждого варианта напишите свой case.
Главное
- Когда
switchразобрал все варианты объединения, вdefaultу значения типnever. const rest: never = s;вdefaultкомпилируется, только пока разобраны все варианты.- Забытый вариант tsc покажет на этой строке:
Type '{ kind: "triangle"; side: number; }' is not assignable to type 'never'. - Запасной ответ в
defaultпрячет забытые варианты от tsc.