Discriminated unions in Typescript: Why is it so good? 🤔 [RU]
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, а поле kind
— discriminant.
Discriminated unions + intersection (&)
Во всех приведенных выше примерах, поле kind
(дискриминант) было единственным общим у вариантов модели. А что, если общих полей больше? Чтобы не дублировать их в каждом варианте, мы можем “вынести их за скобки”, используя intersection (&):
https://gist.github.com/vasilyev-maksim/a791e273b21917be9ddc33b930954829?file=usingIntersection.ts
Итог
Обобщая, можно сказать, что DU позволяют нам более точно описывать структуру вариативных моделей, вместо того, чтоб сваливать в кучу всевозможные поля.
По мне, DU часто недооцениваются, и ими редко пользуются в продуктовой разработке. Я обьясняю это тем, что данная языковая фича встречается далеко не во всех популярных языках. Потому, разработчики не всегда вспоминают о DU, когда те могут пригодиться, либо не знают о них вовсе.
В продолжении этой статьи я делюсь примерами использования DU из реальной практики. Настоятельно советую взглянуть!
Пишите красивый код! Спасибо за внимание 😊