Полиморфизм в TypeScript

Как правило, люди думают, что объектно-ориентированное программирование (ООП) и функциональное программирование (ФП) взаимно исключают друг друга. И это объяснимо: любая дискуссия о них довольно часто превращается в спор, в основе которого лежит соперничество. Но, как оказалось, в некоторых контекстах можно извлечь выгоду, применяя дисциплины из обеих парадигм, и взять таким образом лучшее из двух миров. Один из таких контекстов  —  ad-hoc полиморфизм в Typescript.

Сценарий

Рассмотрим ситуацию, когда мы разрабатываем встроенную библиотеку стилей. Библиотека содержит различные типы, соответствующие разным типам данных CSS, таким как:

  • RGB.
  • RGBA.
  • HSL.
  • HSLA.
  • HEX.
  • Length.
  • Percentage.
  • Decibel.
  • Time.
  • Transformation.

Для простоты наш интерфейс для этих объектов будет поддерживать только одно свойство  —  color.

Как показано на картинке, значение свойства color в нашем интерфейсе имеет тип tagged-union, который состоит из типа stringLiteral и пяти других пользовательских типов, представленных в виде объектов Javascript и смоделированных по модели nominal-typing.

В пределах кода библиотеки, эти типы хорошо понятны, и их значениями можно соответственно управлять, но вне этих границ  —  скажем в React,  —  они уже лишаются смысла. Чтобы React мог пользоваться этими значениями для определения стилей, нам нужно сериализовать их в строковые (string) или числовые (number) значения.

Задача

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

Ничего необычного здесь нет. Мы создаем небольшие функции, каждая из которых отвечает за сериализацию одного подтипа (color), а затем создаем функцию, которая проверяет тип передаваемого аргумента, используя защиту типов, и вызывает функцию, связанную с этим типом. Такой подход приводит к избытку кода и увеличению числа операций импорта (в данном случае их пять).

В Haskell для того, чтобы разобраться с этим сценарием, можно было бы воспользоваться ad-hoc полиморфизмом. В таком случае у вас будут разные реализации под одним и тем же именем, связанные с разными типами, и Haskell автоматически выберет подходящую реализацию в зависимости от типа аргумента. Если бы в Typescript было нечто подобное, оно бы выглядело примерно так (за минусом, разумеется, сообщения об ошибке):

Как видите, компилятор TS жалуется на “дублирующую реализацию функции”. Эта ошибка возникает из-за того, что у вас может быть несколько подписей под одним и тем же именем, но разрешена только одна реализация. Эта реализация отвечает за поиск соответствующей функции для ее вызова. Поэтому, чтобы исправить ошибку, нужно сделать следующее:

И в этом нет никакого смысла! Мы оказались в той же ситуации, что и раньше, только хуже, потому что кода стало больше. Typescript называет это перегрузкой функций. Она может быть удобна во многих случаях, но не в этом.

Перегрузка функций в TS сработала не так, как ожидалось. И тут вы унываете, голос в голове призывает вас обратиться к темной стороне, где бедные объекты кричат от боли, вызванной мутациями и привязкой метода. Но затем, как раз перед тем, как вы сделаете свой первый шаг во вселенную ООП, появляюсь я, ваш ангел-хранитель, и останавливаю вас, предлагая золотую середину  —  безопасное место, где ваши функции вместе с объектами могут процветать в чистоте и довольстве.

Проблеск вселенной ООП

Решение

Мой подход к этой проблеме заключается в том, чтобы сохранить ссылку на функцию внутри типизированного объекта, выкраденного прямиком из вселенной ООП. В этом подходе нет никаких состояний, и функция все еще прозрачна. Упомянутая ссылка предназначена только для того, чтобы обеспечить легкий и быстрый доступ к соответствующей функции и устранить избыточность кода. Мы можем сделать это, добавив свойство, значение которого будет ссылкой на функцию. Что-то вроде такого:

Свойства type и data можно игнорировать. Функция rgb  —  это просто фабричная функция, которая создает значения типа RGB. Результирующий объект будет иметь свойство serialize, указывающее на функцию serializeRGB. Следуя этому шаблону с другими нашими пользовательскими типами, мы можем реорганизовать функцию serializeColor в следующий вид:

Каждый тип указывает на собственную реализацию поведения сериализации. Никакой необходимости в импорте или ручной проверке типов.

Надеюсь, вы согласитесь, что это намного аккуратнее предыдущей версии. Но если и нет, то все в порядке. Мы все имеем право на свое мнение.

Выводы

  • Можно внести некоторые моменты из ООП в функциональный код, не нарушая его чистоту.
  • Ссылки  —  отличный способ реализации полиморфизма типа ad-hoc в ТypeScript.

Сценарий, изложенный в этой статье, на самом деле представляет собой упрощенную версию того, с которым я столкнулся в Rosebox, библиотеке стилей, которую я написал в Typescript. Можете проверить.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи: Hugo Nteifeh, “Polymorphism in Typescript”

Предыдущая статьяОсновные концепции и структуры Python, которые должен знать каждый серьёзный программист
Следующая статьяНаивный байесовский алгоритм