Emulating Higher-Kinded Types in TypeScript: Practical Approaches
TypeScript doesn’t natively support higher-kinded types (HKT), limiting the creation of reusable abstractions over type constructors like Array, Set, or Promise. HKT allow you to parameterize not just the element types but also the container itself, enabling strong typing for functional patterns such as functors and monads. Without them, you end up duplicating code for map-like operations across every data type.
Consider a typical task: implementing map for Set.
type MapSet = <A, B>(f: (a: A) => B, set: Set<A>) => Set<B>;
Similarly, you’d need versions for ReadonlySet, LinkedList, ReadonlyArray. Duplication is unavoidable without HKT, where an interface could simply be Mappable<F> with a signature F<A> → F<B>.
TypeScript throws an error: Type ‘F’ is not generic, because it can’t distinguish between type kinds ( for string and → * for Array).
Emulation via Constructor Dictionary
The simplest workaround is a dictionary mapping string keys to types.
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> resolves to Array<number>. On this foundation, we build a generic type:
type Mappable<F extends keyof TypeConstructors<unknown>> = <A, B>(
f: (a: A) => B,
list: Kind<F, A>
) => Kind<F, B>;
Implementations for specific types:
const mapArray: Mappable<'Array'> = (f, list) => list.map(f);
const mapSet: Mappable<'Set'> = (f, list) => new Set(Array.from(list, f));
Arity Limitations and Solutions
This approach works well for unary constructors ( → ). For binary ones like Map<K, V>, you need a separate dictionary:
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];
This complicates handling multi-arity types. In production projects, developers rely on libraries:
- hkt-toolbelt: Supports up to 4 parameters, includes utilities for functors and monads.
- hkt-core: Focuses on type performance, minimal API surface.
These tools deliver type inference close to native HKT, eliminating the need for manual dictionary management.
Key Challenges in Adopting HKT
Despite ongoing requests (issue #1213, #55280), HKT remain absent as of 2026. Major barriers include:
- Compiler complexity: Introducing kind polymorphism, extending type inference, adapting to structural typing.
- Performance: Increased compilation times in large-scale FP projects.
- Design trade-offs: Balancing expressiveness with JavaScript compatibility and avoiding syntax bloat.
Proposed solutions include:
- A special marker like
F<~>for type constructors. - Enhancing conditional types without new syntax.
- Pragmatic emulation using existing TypeScript features.
What Matters Most
- HKT are emulated via Kind mappings, preserving strict typing for containers.
- Libraries like hkt-toolbelt and hkt-core simplify integration in mid-to-senior level projects.
- Arity requires separate dictionaries; unary types are ideal for foundational use cases.
- Future native support may come through incremental improvements that don’t compromise performance.
- Emulation enables clean implementations of
map,flatMap, and similar functions without code duplication acrossArray,Set,Promise, and more.
— Editorial Team
No comments yet.