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>.
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:
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:
- 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
Brak komentarzy.