Высшие дженерики в typescript
Дженерики позволяют нам писать такие функции.
Но что, если мы хотим, чтобы наша чудесная функция map работала не только с массивами? Что, если мы хотим, чтобы она работала с Array, с Set, и другими коллекциями?
Увы, typescript не позволяет написать такую функцию. Если не использовать черную магию.
Базовая идея
Сначала мы заводим специальный интерфейс, в который будем складывать типы второго порядка:
И каждый раз, когда мы захотим поддерживать новый тип, мы будем использовать внешнее расширение модуля:
Окей, теперь мы можем написать код, создающий generic второго порядка.
И давайте попробуем это использовать:
Строим дальше
Наша функция map должна понимать, как именно создавать коллекцию, как в неё добавлять элементы, и как итерироваться по ней.
Поэтому нам придется построить некоторое описание коллекции:
И вот как выглядит описание коллекции для массива:
Вот теперь мы можем наконец-то построить функцию map. Давайте посмотрим, как она будет использована:
Увы, мы не можем избавиться от необходимости передавать ArrayDescription и SetDescription в map. (Возможно нам помогут трансформеры, но это требует дальнейшего исследования).
Продвинутая идея
Сейчас нам приходится в описании коллекции описывать getIterator. Но вообще-то все коллекции уже реализуют Iterable<>. Так что давайте поступим по-другому. Мы на все типы коллекций заведем ограничение: они должны расширять Iterable<>.
Соответственно, мы начинаем строить специализированный файл hkt.ts. Для начала, мы опять же заводим специальный интерфейс, в который будем складывать типы
И точно так же, как и прежде, мы будем расширять этот интерфейс извне:
И вот теперь начинается веселье! Теперь в автоматическом режимы мы построим новый интерфейс, в который попадут только Iterable-типы.
Для каждого типа в TypeIdToProbableCollectionTypeMap<>:
- Если тип расширяет Iterable<>, оставляем его
- А если нет, то заменяем его сообщением об ошибке
И конечно же, нам нужно предоставить специализированные версии 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.