Discriminated unions in Typescript: Use cases [RU]

Vasilyev Maksim
3 min readJan 19, 2021

--

This article is also available in english.

В первой части статьи я рассказал про то, чем полезны discriminated unions (далее DU). Советую ознакомиться с ней, если не читали. Здесь я расскажу про то, как можно применять их на практике.

Асинхронные данные

Классика. Подавляющее большинство клиентских приложений обращаются за данными на сервер. IAsyncData описывает этот процесс.

Скажу по опыту, что данный подход спасает от “null ref” ошибок в react-приложениях (и не только). Вот почему:

Представим, что у нас есть “плохой” вариант (без DU) асинхронного списка пользователей asyncUsers, который передается в компоненты через контекст. Есть два компонента: Child и Parent. Оба берут пользователей из контекста. Child рендерит список пользователей в виде таблицы. Parent, в зависимости от статуса, рендерит либо Child, либо индикатор загрузки.

Значит, Child рендерится только если asyncUsers.status == 'success'. Тогда, есть ли смысл заново проверять статус в Child, или можно напрямую обращаться к asyncUsers.data? Казалось бы, что можно… До тех пор, пока мы не переместим Child на уровень выше или рядом с Parent — там, где никто не позаботился проверить статус за Child. Тут нас и ждет коварная “null ref” ошибка.

Если использовать “хорошую” модель (с DU), typescript вынудит нас проверять asyncUsers.status перед обращением к asyncUsers.data в каждом компоненте. Вуаля, никаких ошибок! 😉

Валидация

Я часто встречаю следующий подход при валидации: если данные валидны, функция/класс валидации ничего не возвращает, иначе — бросает исключение, внутри которого список ошибок. По мне, это противоречит самой сути исключений. Исключение надо бросать тогда, когда ситуация исключительная (еще раз прошу прощения за тавтологию 😁). Невалидные данные — один из вполне ожидаемых сценариев. На то и нужна валидация, чтоб проводить черту между валидным и невалидным.

Вместо того, чтоб бросать исключение или ничего не возвращать, можно всегда возвращать объект следующего типа:

Контракт с сервером (DTO)

Зачастую модели на фронтенде могут быть точнее моделей на бэкенде. Дело в том, что часто язык, на котором написан бэкенд, не поддерживает DU.

К примеру, мы работаем над системой документооборота. Бизнес просит нас реализовать функционал подписи документа. Так же, надо показать в интерфейсе дату подписи.

В первую очередь, бэкенд разработчики добавили новый статус signed в модель документа. Осталось добавить дату подписи signedAt. Если бэкенд написан на Java, то единственное, что можно сделать с signedAt — сделать его опциональным и добавить ко всем остальным полям. Но на фронтенде необязательно делать так же, ведь у нас есть DU 😏

Также, если мы имеем дело с версионированным API, то, используя DU, можно описать все версии модели одним типом:

Flow-based API design

Flow-based API design — альтернативный способ дизайна классов-сервисов и один из ярких примеров элегантного использования DU. Расскажу что это такое в двух словах, но очень советую ознакомиться с оригинальной статьей, содержащей реальные примеры использования.

Предположим, есть сервис с тремя асинхронным методами: a,b и c. По бизнес логике, вызывать методы следует только в алфавитном порядке: abc. Иной порядок вызова смысла не имеет. Вот как выглядят “классический” и “flow-based” сервисы:

Второй вариант отражает в себе правильную последовательность вызовов, а первый — нет.

Пишите элегантный код! Спасибо за внимание 😊

--

--