Discriminated unions in Typescript: Why is it so good? 🤔 [RU]

Vasilyev Maksim
3 min readJan 19, 2021

This article is also available in english.

Приветствую! ✋

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

Если вы не знакомы с DU, то вот вам простое объяснение и документация. Так же, для полного понимания статьи желательно знать, что такое обычный union(|) и intersection(&).

Чем они полезны?

Приведу пример. Вообразим, что у нас есть свой кулинарный блог. Посетители нашего блога могут оставлять комментарии к постам. Автор комментария может быть анонимом, либо предварительно пройти регистрацию и стать полноценным пользователем с именем и аватаром.

Вот как выглядит модель комментария к посту:

Описать модель автора комментария IAuthor можно по-разному. Не забываем, что имя и аватар могут быть только у пользователя приложения, которые он предоставил во время регистрации. Аноним же остается анонимным (прошу прощения за тавтологию 😁).

Попытка №1

Вроде бы, все хорошо — name и avatarUrl опциональны, то есть могут отсутствовать. Почему? Потому что пользователь может быть ананимом. Но единственная ли это причина? Нет.

Есть сценарий, о котором все так и норовят забыть — ошибка сервера. Возможно, кто-то случайно стер имя конкретного пользователя из базы данных. Либо оно, и вовсе, не записалось во время регистрации. Всякое бывает 🤷‍♂️.

Вопрос в том, как мы реагируем на два этих сценария. В случае анонимности автора можно написать “anonym” над текстом комментария. В случае нарушения целостности данных на сервере можно написать “unknown”, но, в то же время, отрисовать аватар. Более того, можно отправить на лог-сервер сообщение о невалидных данных, что поможет быстрее обнаружить и исправить ошибку.

Раз мы реагируем на эти два сценария по-разному, мы должны их как-то отличать. Пробуем дальше.

Попытка №2

Добавленное поле kind явно сообщает нам тип автора, и, как следствие, что нам делать, если отсутствует name.

Но есть одна проблема. Typescript не вынуждает нас убедиться в том, что автор является пользователем, до того как мы потянемся в коде за name. Поле name доступно всегда. Увидев это, новый разработчик может даже и не понять, что в приложнии есть тип авторов, у которых не бывает имени.

Попытка №3 (Решение)

Что же нам делать? Использовать DU!

Обратите внимание на то, что name и avatarUrl больше не опциональны (нет ?).

Теперь typescript не даст нам обратиться к name, не проверив kind. Так как была проверка comment.author.kind === ‘user’, автор перестаёт быть суперпозицией анонима и пользователя внутри if, и typescript воспринимает его как пользователя, разрешая доступ к полям name и avatarUrl. Приём с if называется type narrowing, а поле kinddiscriminant.

Discriminated unions + intersection (&)

Во всех приведенных выше примерах, поле kind (дискриминант) было единственным общим у вариантов модели. А что, если общих полей больше? Чтобы не дублировать их в каждом варианте, мы можем “вынести их за скобки”, используя intersection (&):

https://gist.github.com/vasilyev-maksim/a791e273b21917be9ddc33b930954829?file=usingIntersection.ts

Итог

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

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

В продолжении этой статьи я делюсь примерами использования DU из реальной практики. Настоятельно советую взглянуть!

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

--

--