Hkts
A simple encoding of higher-kinded types in TypeScript
Install / Use
/learn @pelotom/HktsREADME
HKTS - Higher-Kinded TypeScript 
Overview
TypeScript doesn't directly support higher-kinded types yet, but various attempts have been made to simulate them (see related work at the bottom). This project presents a new, greatly simplified approach to encoding HKTs using the power of conditional types.
The idea is that, although we can't truly abstract over a type constructor type T<A> = ..., we can abstract over the result T<_> of applying it to a special placeholder type _. Then, if we can somehow substitute all instances of _ within a type, we effectively have the ability to "apply" T at arbitrary types. That is, we can abstract over T! And it turns out we can define a substitution operator $<T, S> which does just that.
Here's how we would use $ to define the static-land's Functor type class:
interface Functor<T> {
map: <A, B>(f: (x: A) => B, t: $<T, [A]>) => $<T, [B]>;
}
Then, supposing we have a Maybe type constructor
type Maybe<A> = { tag: 'none' } | { tag: 'some'; value: A };
const none: Maybe<never> = { tag: 'none' };
const some = <A>(value: A): Maybe<A> => ({ tag: 'some', value });
we can define a Functor instance for it like so, using the placeholder type _:
const MaybeFunctor: Functor<Maybe<_>> = {
map: (f, maybe) => maybe.tag === 'none' ? none : some(f(maybe.value)),
};
// It works!
expect(MaybeFunctor.map(n => n + 1, some(42))).toEqual(some(43));
Type classes and instance factories
This package defines a set of interfaces corresponding to the the type classes of the static-land spec, as well as factory functions for producing instances thereof. For example, there is a Monad interface as well as a Monad function. The function takes as arguments only the minimum data (of and chain) needed to produce an implementation of the full Monad interface (which includes other derived methods like map, ap and join). So again using the Maybe type above, we can construct a Monad instance like so:
const MaybeMonad = Monad<Maybe<_>>({
of: some,
chain: (f, maybe) => maybe.tag === 'none' ? none : f(maybe.value),
});
// Use the `map` method, which we didn't have to define:
expect(MaybeMonad.map(n => n + 1, some(42))).toBe(some(43));
Abstracting over kinds of higher arity
You may have noticed in the above example that $ takes an array of substitution types as its second parameter. There are also placeholders _0 (an alias for _), _1, _2, ..., _<N>. $<T, S> simultaneously replaces _0 with S[0], _1 with S[1], and so on, throughout T. This allows us to define, for example, Bifunctor, which abstracts over a type constructor of kind (*, *) -> *:
interface Bifunctor<T> {
bimap: <A, B, C, D>(f: (x: A) => B, g: (x: C) => D, t: $<T, [A, C]>) => $<T, [B, D]>;
// ...
}
Then given an Either type constructor
type Either<A, B> = { tag: 'left'; left: A } | { tag: 'right'; right: B };
const left = <A>(left: A): Either<A, never> => ({ tag: 'left', left });
const right = <B>(right: B): Either<never, B> => ({ tag: 'right', right });
a Bifunctor instance for it looks like
type EitherBifunctor: Bifunctor<Either<_0, _1>> = {
bimap: (f, g, either) => (either.tag === 'left' ? left(f(either.left)) : right(g(either.right))),
};
Fixing type parameters
Suppose we want to ignore one or more of the parameters of a type constructor for the purpose of making it an instance of a type class. For example we can make a Monad out of Either by ignoring its first parameter and using the second parameter as the "hole" of the monad. The way to do this is to make a polymorphic instance creation function which can produce a Monad instance for any given left type L:
const RightMonad = <L>() => Monad<Either<L, _>>({
of: right,
chain: (f, either) => either.tag === 'left' ? either : f(either.right),
});
Known limitations
The type application operator $ is able to transform most types correctly, including functions, however there are a few edge cases:
- Polymorphic functions will get nerfed. For example:
Not what you wanted! Unfortunately TypeScript's conditional types don't currently allow analyzing and reconstructing the type parameters of a function (this feature might solve that).type Id = <A>(x: A) => A type NerfedId = $<Id, []>; // type NerfedId = (x_0: {}) => {} - Tuples
[A, B, ...]are transformed correctly up to size 10 (though we can add arbitrarily many more as needed), after which they will be transformed into an array(A | B | ...)[]. Correspondingly, functions of arity <= 10 are supported. - In some cases the
joinmethod ofMonadmust be supplied a type parameter... I've filed an issue about this. The good news is it's still safe; you can't provide a wrong type argument without getting an error.
Related work
Other notable attempts to solve this problem:
- https://medium.com/@gcanti/higher-kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe
- https://github.com/SimonMeskens/TypeProps
Related Skills
node-connect
347.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
