Брендированные типы в TypeScript

При моделировании сущностей с помощью TypeScript очень часто получается интерфейс следующего вида:

TypeScript

 

Проблема

Типы свойств не имеют семантического значения. С точки зрения типов User.id, Order.id, Order.year и т. д. одинаковы: число, и как число они взаимозаменяемы, но семантически они различаются.

Исходя из предыдущего примера, мы можем иметь набор функций, которые выполняют действия над сущностями. Например:

TypeScript

 

Эти функции будут принимать любое число в любом аргументе, независимо от семантического значения числа. Например:

TypeScript

 

Очевидно, что это большая ошибка, и кажется, что можно избежать ее, прочитав код, но код не всегда так прост, как в примере.

То же самое происходит с getOrdersFiltered: мы можем поменять значения дня и месяца, и не получим никакого предупреждения или ошибки. Ошибки возникнут, если день больше 12, но очевидно, что результат не будет ожидаемым.

Решение

Правила объектной калистеники предоставляют решение для этого: обернуть все примитивы и строки (связанный с антипаттерном Primitive obsession). Правило заключается в обертывании примитивов объектом, который представляет семантическое значение (DDD описывает это как ValueObjects).

Но с TypeScript нам не нужно использовать классы или объекты для этого: мы можем использовать систему типов, чтобы гарантировать, что число, представляющее что-то отличное от года, не может быть использовано вместо года.

Маркированные типы

Этот шаблон использует расширяемость типов для добавления свойства, которое обеспечивает семантическое значение:

TypeScript

 

Эта простая строка создаёт новый тип, который может работать как число — но не является числом, это год.

TypeScript

 

Обобщение решения

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

TypeScript

 

Который использует уникальный символ в качестве имени свойства бренда, чтобы избежать конфликтов с вашими свойствами, и получает оригинальный тип и бренд в качестве обобщённых параметров.

С этим мы можем рефакторить наши модели и функции следующим образом:

TypeScript

 

Теперь, в этом примере, IDE покажет ошибку, так как id — это UserId, а deleteOrder ожидает OrderId.

TypeScript

 

Компромиссы

В качестве небольшого компромисса вам нужно будет использовать X как Brand. Например, const year = 2012 as Year при создании нового значения из примитива, но это эквивалентно new Year(2012), если вы используете объект значения. Вы можете предоставить функцию, которая будет работать как своего рода “конструктор”:

TypeScript

 

Валидация с помощью брендированных типов

Брендированные типы также полезны для обеспечения валидности данных, так как вы можете иметь специфические типы для валидированных данных, и вы можете доверять, что пользователь был валидирован, просто используя типы:

TypeScript

 

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

Резюме

Брендированные типы — это простое решение, которое включает следующее:

  • Улучшает читаемость кода: Делает яснее, какое значение должно быть использовано в каждом аргументе
  • Надежность: Помогает избежать ошибок в коде, которые могут быть трудными для обнаружения; теперь IDE (и проверка типов) помогают нам выявить, находится ли значение в правильном месте
  • Валидация данных: Вы можете использовать брендированные типы, чтобы гарантировать, что данные действительны.

Вы можете рассматривать брендированные типы как своего рода версию ValueObjects, но без использования классов — только типы и функции.

Наслаждайтесь мощью типизации!

Source:
https://dzone.com/articles/branded-types-in-typescript