Вместо того чтобы сразу погружаться в теорию, рассмотрим практический пример.
Требования
- Необходимо создать функцию, которая возвращает одну из 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;
*Единственное ли это преимущество размеченных объединений? Нет, вот еще несколько других плюсов.
- Облегчение рефакторинга. При рефакторинге кода размеченные объединения облегчают выявление всех мест, где используется тот или иной тип. Это позволяет сэкономить время и снизить риск внесения ошибок при рефакторинге.
- Обеспечение исчерпывающих проверок. TypeScript может проверять, все ли возможные типы в объединении обработаны, выдавая предупреждения или ошибки при отсутствии таких случаев. Это помогает предотвратить непреднамеренные пропуски при работе со сложными структурами данных.
- Облегчение сопоставления шаблонов. Размеченные объединения часто используются в сочетании с операторами
switch
или условными проверками, что позволяет сопоставлять шаблоны в TypeScript. Это облегчает деструктуризацию параметров и позволяет структурировано и организовано обрабатывать различные случаи или вариации типов. - Создание более простого для понимания кода. Свойство discriminant (“дискриминант”) служит четким индикатором типа объекта в объединении. Таким образом, код становится более читабельным и понятным для специалистов, особенно при работе со сложными структурами данных.
- Улучшение практики заполнения кода. IDE и редакторы кода могут предоставлять более точные предложения по заполнению кода при работе с объектами, которые являются частью размеченных объединений. Это происходит потому, что TypeScript способен выводить конкретный тип на основе свойства discriminant. Если вы используете что-то вроде GitHub Copilot, то автозаполнение поможет писать код быстрее и сделает этот процесс намного приятнее.
Читайте также:
- Компиляция TypeScript в нативный код
- Профессиональная обработка ошибок в TypeScript
- Как профессионально использовать сопоставимые типы TypeScript
Читайте нас в Telegram, VK и Дзен
Перевод статьи Mhetre Ayush: Did you know about Discriminated Unions in TypeScript?