Размеченные объединения
Сортировку на ленте можно сделать ещё проще. При отправке на каждую посылку клеят этикетку с одним словом: «конверт» или «банка». Арке больше не нужно разглядывать посылку и искать приметы. Она читает этикетку и сразу знает, что внутри и в какой жёлоб это отправить.
В программе так же различают объекты. Фигура бывает кругом с радиусом r или квадратом со стороной side. Прочитать side у любой фигуры нельзя:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number };
function area(s: Shape) {
return s.side ** 2; // ✗
}error TS2339: Property 'side' does not exist on type 'Shape'.
Property 'side' does not exist on type '{ kind: "circle"; r: number; }'.У круга нет стороны, а в s может прийти круг. Помогла бы проверка "side" in s, но в TypeScript есть приём удобнее — этикетка в самом объекте. Она уже есть в этом примере: поле kind.
Общее поле с литеральным типом
В каждом варианте объединения объявлено одно и то же поле kind, и у каждого варианта у него свой литеральный тип: у круга "circle", у квадрата "square". Такое объединение называют размеченным объединением, а общее поле — его признаком. Сравнение признака с литералом сужает тип:
type Shape =
| { kind: "circle"; r: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
if (s.kind === "circle") {
return Math.PI * s.r ** 2;
}
return s.side ** 2;
}
console.log(area({ kind: "square", side: 3 })); // 9Давайте разберём пример по строкам:
- В
type Shapeу каждого варианта своё значениеkind. Вертикальную черту можно поставить и перед первым вариантом: так длинное объединение удобно записывать столбиком. s.kind === "circle"сравнивает признак с литералом. Значение"circle"бывает только у круга, поэтому внутриifуsтип круга, иs.rкомпилируется.- После
returnостался квадрат, иs.sideтоже компилируется. - Программа печатает
9.
Главное здесь — проверка одного поля сужает весь объект. Узнав kind, tsc знает и остальные поля варианта: у круга есть r, у квадрата — side.
switch по признаку
Когда вариантов несколько, признак удобно разбирать в switch. Каждый case сужает объект до своего варианта:
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;
}
}
console.log(label({ kind: "circle", r: 2 })); // circle, r = 2Внутри case "circle" у s тип круга, внутри case "square" — квадрата. TypeScript видит, что два case разобрали все варианты, и выводит для label тип результата string. Программа печатает circle, r = 2.
Этот вопрос часто задают на собеседовании: что такое размеченное объединение и как его сузить. Ответ: у всех вариантов есть общее поле с разными литеральными типами, и проверка этого поля в if или switch сужает объект целиком.
Здесь аналогия с этикеткой перестаёт работать. Настоящую этикетку отправитель может написать с ошибкой, и сортировка поверит ей. Признак проверяет tsc: объект { kind: "triangle", side: 3 } не пройдёт как Shape, потому что "triangle" не подходит к типу "circle" | "square". А при запуске признак — обычное поле объекта, поэтому сравнение s.kind === "circle" работает и в JavaScript.
Частая ошибка
Признаку дали тип string:
type Shape =
| { kind: string; r: number }
| { kind: string; side: number };
function area(s: Shape): number {
if (s.kind === "circle") {
return Math.PI * s.r ** 2; // ✗
}
return 0;
}error TS2339: Property 'r' does not exist on type 'Shape'.У обоих вариантов признак — любая строка, и сравнение с "circle" ничего не говорит о варианте: строка "circle" подходит обоим. tsc не может сузить объект и не даёт прочитать s.r. Признаку нужны литеральные типы, разные у каждого варианта: kind: "circle" и kind: "square".
Главное
- В размеченном объединении у всех вариантов есть общее поле-признак, и у каждого варианта в нём свой литеральный тип.
- Проверка признака в
ifилиswitchсужает весь объект, вместе с полями варианта. - Признак — обычное поле: оно остаётся в объекте при запуске, и сравнение работает в JavaScript.
- С признаком типа
stringсужения не будет: нужны разные литеральные типы.