Высшие дженерики в typescript

Viktor Love
3 min readSep 2, 2021

--

Дженерики позволяют нам писать такие функции.

Но что, если мы хотим, чтобы наша чудесная функция map работала не только с массивами? Что, если мы хотим, чтобы она работала с Array, с Set, и другими коллекциями?

Увы, typescript не позволяет написать такую функцию. Если не использовать черную магию.

Базовая идея

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

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

Окей, теперь мы можем написать код, создающий generic второго порядка.

И давайте попробуем это использовать:

Строим дальше

Наша функция map должна понимать, как именно создавать коллекцию, как в неё добавлять элементы, и как итерироваться по ней.

Поэтому нам придется построить некоторое описание коллекции:

И вот как выглядит описание коллекции для массива:

Вот теперь мы можем наконец-то построить функцию map. Давайте посмотрим, как она будет использована:

Увы, мы не можем избавиться от необходимости передавать ArrayDescription и SetDescription в map. (Возможно нам помогут трансформеры, но это требует дальнейшего исследования).

Продвинутая идея

Сейчас нам приходится в описании коллекции описывать getIterator. Но вообще-то все коллекции уже реализуют Iterable<>. Так что давайте поступим по-другому. Мы на все типы коллекций заведем ограничение: они должны расширять Iterable<>.

Соответственно, мы начинаем строить специализированный файл hkt.ts. Для начала, мы опять же заводим специальный интерфейс, в который будем складывать типы

И точно так же, как и прежде, мы будем расширять этот интерфейс извне:

И вот теперь начинается веселье! Теперь в автоматическом режимы мы построим новый интерфейс, в который попадут только Iterable-типы.

Для каждого типа в TypeIdToProbableCollectionTypeMap<>:

  1. Если тип расширяет Iterable<>, оставляем его
  2. А если нет, то заменяем его сообщением об ошибке

И конечно же, нам нужно предоставить специализированные версии TypeIds и MakeType, который будут использовать уже только Iterable-типы.

Ограничения

Увы, это не работает, когда у типов разные ограчения. Например, мы не можем написать функцию, которая работает и с Set, и с WeakSet. Просто потому что Set<T extends any>, а WeakSet<T extends object>.

Исходники

Некоторые детали реализации я опустил из статьи. Если что-то неясно, то вы можете почитать код полной реализации в моем репозитории “vlova / ts-higher-kinded-polymorphism-sample

Авторство идеи

Наивную непонятную реализацию я нашел здесь: “fp-ts / guides / write type class instance”. Ребята говорят, что использовали идею из статьи “Lightweight higher-kinded polymorphism (pdf)”, в которой расказан этот трюк для OCaml.

--

--

Viktor Love
Viktor Love

Written by Viktor Love

Software Engineer from Ukraine. TypeScript, React, C#, Angular.

No responses yet