For people coming across this, there's this nice example floating around on the TypeScript discord server:
export interface Hkt<I = unknown, O = unknown> {
[Hkt.isHkt]: never,
[Hkt.input]: I,
[Hkt.output]: O,
}
export declare namespace Hkt {
const isHkt: unique symbol
const input: unique symbol
const output: unique symbol
type Input<T extends Hkt<any, any>> =
T[typeof Hkt.input]
type Output<T extends Hkt<any, any>, I extends Input<T>> =
(T & { [input]: I })[typeof output]
interface Compose<O, A extends Hkt<any, O>, B extends Hkt<any, Input<A>>> extends Hkt<Input<B>, O>{
[output]: Output<A, Output<B, Input<this>>>,
}
interface Constant<T, I = unknown> extends Hkt<I, T> {}
}
Which can be used as follows. The snippet below defines a SetFactory
, where you specify the type the desired set type when creating a factory, e.g. typeof FooSet
or typeof BarSet
. typeof FooSet
is the constructor for a FooSet
and is like a higher kinded type, the constructor type takes any T
and returns a FooSet<T>
. The SetFactory
contains several methods such as createNumberSet
, which returns a new set of the given type, with the type parameters set to number
.
interface FooSetHkt extends Hkt<unknown, FooSet<any>> {
[Hkt.output]: FooSet<Hkt.Input<this>>
}
class FooSet<T> extends Set<T> {
foo() {}
static hkt: FooSetHkt;
}
interface BarSetHkt extends Hkt<unknown, BarSet<any>> {
[Hkt.output]: BarSet<Hkt.Input<this>>;
}
class BarSet<T> extends Set<T> {
bar() {}
static hkt: BarSetHkt;
}
class SetFactory<Cons extends {
new <T>(): Hkt.Output<Cons["hkt"], T>;
hkt: Hkt<unknown, Set<any>>;
}> {
constructor(private Ctr: Cons) {}
createNumberSet() { return new this.Ctr<number>(); }
createStringSet() { return new this.Ctr<string>(); }
}
// SetFactory<typeof FooSet>
const fooFactory = new SetFactory(FooSet);
// SetFactory<typeof BarSet>
const barFactory = new SetFactory(BarSet);
// FooSet<number>
fooFactory.createNumberSet();
// FooSet<string>
fooFactory.createStringSet();
// BarSet<number>
barFactory.createNumberSet();
// BarSet<string>
barFactory.createStringSet();
Short explanation of how this works (with FooSet
and number
as an example):
- The main type to understand is
Hkt.Output<Const["hkt"], T>
. With our example types substituted this becomes Hkt.Output<(typeof FooSet)["hkt"], number>
. The magic now involves turning this into a FooSet<number>
- First we resolve
(typeof FooSet)["hkt"]
to FooSetHkt
. A lot of the magic lies here, by storing the info about how to create a FooSet
in the static hkt
property of FooSet
. You need to do this for each supported class.
- Now we have
Hkt.Output<FooSetHkt, number>
. Resolving the Hkt.Output
type alias, we get (FooSetHkt & { [Hkt.input]: number })[typeof Hkt.output]
. The unique symbols Hkt.input
/ Hkt.output
help for creating unique properties, but we could have also used unique string constants.
- Now we need to access the
Hkt.output
property of FooSetHkt
. This is different for each class and contains the details on how to construct a concrete type with the type argument. FooSetHkt
defines the output property to be of type FooSet<Hkt.Input<this>>
.
- Finally,
Hkt.Input<this>
just acceses the Hkt.input
property of FooSetHkt
. It would resolve to unknown
, but by using the intersection FooSetHkt & { [Hkt.input]: number }
, we can change the Hkt.input
property to number
. And so if we've reached our goal, Hkt.Input<this>
resolves to number
and FooSet<Hkt.Input<this>>
resolves to FooSet<number>
.
For the example from the question, Hkt.Output
is essentially what was being asked for, just with the type parameters reversed:
interface List<T> {}
interface ListHkt extends Hkt<unknown, List<any>> {
[Hkt.output]: List<Hkt.Input<this>>
}
type HigherOrderTypeFn<T, M extends Hkt> = Hkt.Output<M, T>;
// Gives you List<number>
type X = HigherOrderTypeFn<number, ListHkt>;