Вместо того чтобы сразу погружаться в теорию, рассмотрим практический пример.

Требования

  1. Необходимо создать функцию, которая возвращает одну из 3 фигур (квадрат, прямоугольник или круг).

2. Функция должна принимать только соответствующие параметры.

3. Параметры для каждой фигуры разные, и они следующие:

  • круг: радиус (“radius”);
  • квадрат: размер (“size”);
  • прямоугольник: высота и ширина (“height & width”).

Один из самых простых способов сделать это в TypeScript  —  создать тип следующим образом:

type CustomShapeProps = {
kind: "square" | "rectangle" | "circle";
size?: number;
width?: number;
height?: number;
radius?: number;
};

Это рабочий вариант, но замечаете ли вы какие-нибудь изъяны в приведенном коде? Посмотрите на следующий GIF-файл, чтобы увидеть, как будет функционировать код, использующий этот тип:

Автозаполнение для типа без размеченного объединения

Как видите, все параметры допустимы независимо от “вида(“kind”) формы.

А что если скомпилировать этот TypeScript-код? Но прежде чем перейти к результатам, посмотрим, как выглядит код для функции “getCustomShape”.

Код для функции getCustomShape:

Результаты компиляции кода:

Результат без размеченных объединений

Вспомним требования:

Функция должна принимать только соответствующие параметры.

Зная полученные результаты и принимая во внимание вышеупомянутое, мы можем сделать следующий вывод.

  • Не только свойство “radius”, которого нет у формы “квадрат” (“square”), может быть передано без каких-либо ошибок/предупреждений, но и передача свойства “size”, которое требуется форме “квадрат”, не является обязательной.

Посмотрим на возможный случай реального сеанса отладки:

// Отладка всех участков кода, в которых используются параметры.
console.log("CustomShape.tsx -> Line 874", {radius}) // radius: undefined
console.log("CustomShape.tsx -> Line 591", {radius}) // radius: undefined
console.log("CustomShape.tsx -> Line 369", {radius}) // radius: undefined

// Мы узнали следующее.
// Изначально форма ("shape") имела вид "квадрат" ("square") и, следовательно,
// получала свойство "size".
// Но вам нужно было сделать ее "кругом" ("circle"),
// поэтому вы изменили вид на "круг", но
// но не обновили передаваемые свойства ("props").
// Компилятор TypeScript также не возражал и, следовательно,
// "radius" везде стал "undefined".

// console.log({radius}) прописывается,
// чтобы вывести как имя, так и значение радиуса.
// Это нужно для того, чтобы следующая строка показывала:
// console.log("radius", radius)

Параметр “size” имеет тип number | undefined, но согласно требованиям, он всегда должен быть defined и иметь тип number (в том случае, если вид (“kind”) является “square”). Это также относится ко всем остальным параметрам, а именно “radius” и “height & width” относительно “kind”.

Теперь, когда мы знаем проблему, рассмотрим одно из самых простых ее решений.

Размеченные объединения в TypeScript

Для начала взглянем на результаты и код, а затем поговорим о том, что такое размеченные объединения и как их реализовывать.

Результат с использованием размеченных объединений:

Поведение IDE:

Автозаполнение с размеченным объединением

Примечание:

*Содержание разделов, отмеченных “*”, было сгенерировано с помощью ChatGPT, чтобы объяснить суть в самых доступных выражениях.

*Итак, что же такое размеченные объединения простыми словами?

  • В TypeScript размеченные объединения позволяют объединить различные типы в один тип, который может представлять несколько возможностей.
  • Это достигается путем добавления общего свойства к каждому типу в объединении, называемого дискриминантом, что помогает TypeScript сузить круг возможных типов в объединении.

Разберемся в этом всем на примере наших фигур.

  • Есть 3 типа фигур  —  квадрат, круг и прямоугольник.
  • Что может быть общим “ключом” или дискриминантом для указанных фигур? Этот ключ  —  “kind”, который используется в качестве идентификатора вида фигуры.

Вооружившись этими знаниями, реализуем их на практике.

Можно создать тип размеченного объединения следующим образом:

type CustomShapeWithDiscriminatedUnion =
| {
kind: "square";
size: number;
}
| {
kind: "rectangle";
width: number;
height: number;
}
| {
kind: "circle";
radius: number;
};

Проведем рефакторинг, создав отдельные типы для каждой формы:

type TSquare = {
kind: "square";
size: number;
};

type TRectangle = {
kind: "rectangle";
width: number;
height: number;
};

type TCircle = {
kind: "circle";
radius: number;
};

type CustomShapeWithDiscriminatedUnion = TSquare | TRectangle | TCircle;

*Единственное ли это преимущество размеченных объединений? Нет, вот еще несколько других плюсов.

  1. Облегчение рефакторинга. При рефакторинге кода размеченные объединения облегчают выявление всех мест, где используется тот или иной тип. Это позволяет сэкономить время и снизить риск внесения ошибок при рефакторинге.
  2. Обеспечение исчерпывающих проверок. TypeScript может проверять, все ли возможные типы в объединении обработаны, выдавая предупреждения или ошибки при отсутствии таких случаев. Это помогает предотвратить непреднамеренные пропуски при работе со сложными структурами данных.
  3. Облегчение сопоставления шаблонов. Размеченные объединения часто используются в сочетании с операторами switch или условными проверками, что позволяет сопоставлять шаблоны в TypeScript. Это облегчает деструктуризацию параметров и позволяет структурировано и организовано обрабатывать различные случаи или вариации типов.
  4. Создание более простого для понимания кода. Свойство discriminant (“дискриминант”) служит четким индикатором типа объекта в объединении. Таким образом, код становится более читабельным и понятным для специалистов, особенно при работе со сложными структурами данных.
  5. Улучшение практики заполнения кода. IDE и редакторы кода могут предоставлять более точные предложения по заполнению кода при работе с объектами, которые являются частью размеченных объединений. Это происходит потому, что TypeScript способен выводить конкретный тип на основе свойства discriminant. Если вы используете что-то вроде GitHub Copilot, то автозаполнение поможет писать код быстрее и сделает этот процесс намного приятнее.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Mhetre Ayush: Did you know about Discriminated Unions in TypeScript?

Предыдущая статья8 советов, которые сделают JavaScript-код чище
Следующая статьяНовая большая речевая модель Watson от IBM предоставит голос генеративному ИИ