Powrót do strony głównej

HKT w TypeScript: emulacja i praktyka

Artykuł omawia emulację Higher-Kinded Types w TypeScript za pomocą mapowań Kind i bibliotek. Opisano ograniczenia arności, implementacje map dla kontenerów i bariery natywnego wsparcia. Przydatne do tworzenia typobezpiecznych FP-abstrakcji.

Emulacja HKT w TypeScript: Kind i biblioteki
Advertisement 728x90

Emulacja typów wyższego rzędu w TypeScript: praktyczne podejścia

TypeScript nie obsługuje typów wyższego rzędu (Higher-Kinded Types, HKT) natywnie, co ogranicza tworzenie uniwersalnych abstrakcji nad konstruktorami typów takimi jak Array, Set czy Promise. HKT pozwalają parametryzować nie tylko typy elementów, ale także same kontenery, zapewniając silną typizację dla wzorców funkcyjnych – funktorów i monad. Bez nich trzeba powtarzać kod dla operacji typu map na każdym typie danych.

Rozważmy typowy problem: implementacja funkcji map dla Set.

type MapSet = <A, B>(f: (a: A) => B, set: Set<A>) => Set<B>;

Podobnie potrzebne są wersje dla ReadonlySet, LinkedList, ReadonlyArray. Powtarzanie kodu jest nieuniknione bez HKT, gdzie interfejs mógłby wyglądać jako Mappable<F> z F<A> → F<B>.

Google AdInline article slot

TypeScript zgłasza błąd: Type ‘F’ is not generic, ponieważ nie rozróżnia rodzajów typów ( dla string i → * dla Array).

Emulacja za pomocą słownika konstruktorów

Najprostsze obejście to słownik przypisujący klucze tekstowe do typów.

interface TypeConstructors<A> {
  'Array': Array<A>;
  'LinkedList': LinkedList<A>;
  'ReadonlySet': ReadonlySet<A>;
  'Set': Set<A>;
}

type Kind<F extends keyof TypeConstructors<unknown>, A> = TypeConstructors<A>[F];

Kind<'Array', number> zostaje wyprowadzony jako Array<number>. Na jego podstawie buduje się uniwersalny typ:

Google AdInline article slot
type Mappable<F extends keyof TypeConstructors<unknown>> = <A, B>(
  f: (a: A) => B,
  list: Kind<F, A>
) => Kind<F, B>;

Realizacje dla konkretnych typów:

const mapArray: Mappable<'Array'> = (f, list) => list.map(f);
const mapSet: Mappable<'Set'> = (f, list) => new Set(Array.from(list, f));

Ograniczenia arności i rozwiązania

Ten sposób działa dla konstruktorów jednoargumentowych (). Dla dwuargumentowych (Map<K, V>) potrzebny jest osobny słownik:

interface TypeConstructors2<A, B> {
  Map: Map<A, B>;
  Record: Record<A, B>;
}

type Kind2<F extends keyof TypeConstructors2<unknown, unknown>, A, B> = TypeConstructors2<A, B>[F];

To komplikuje obsługę typów różnej arności. W projektach produkcyjnych stosuje się biblioteki:

Google AdInline article slot
  • hkt-toolbelt: obsługa do 4 parametrów, narzędzia do funktorów i monad.
  • hkt-core: skupienie na wydajności typów, minimalistyczne API.

Te rozwiązania zapewniają wyprowadzanie typów zbliżone do natywnego HKT bez ręcznego słownika.

Kluczowe wyzwania wdrożenia HKT

Mimo licznych prośb (issue #1213, #55280), HKT brakuje w 2026 roku. Głównymi barierami są:

  • Złożoność kompilatora: wprowadzenie rodzajów, rozbudowa wnioskowania typów, dostosowanie do strukturalnej typizacji.
  • Wydajność: wzrost czasu kompilacji w dużych projektach funkcyjnych.
  • Dyszygnatura: równowaga między wyrazistością a zgodnością z JS, unikanie przepełnienia składni.

Propozycje obejmują:

  • specjalny marker F<~> dla konstruktorów.
  • ulepszenie typów warunkowych bez nowej składni.
  • praktyczne emulacje przez istniejące mechanizmy.

Co warto wiedzieć

  • HKT emulowane są poprzez mapowanie Kind, zapewniając silną typizację dla kontenerów.
  • Biblioteki hkt-toolbelt/hkt-core upraszczają integrację w projektach średnio- i wysokopoziomowych.
  • Arność wymaga oddzielnych słowników; typy jednoargumentowe to priorytet dla podstawowych przypadków.
  • Przyszła obsługa może być możliwa dzięki kompromisowym ulepszeniom, bez naruszania wydajności.
  • Emulacja pozwala realizować map, flatMap bez duplikacji dla Array/Set/Promise.

— Editorial Team

Advertisement 728x90

Czytaj dalej