Please consider the following pseudo-code trying to define a higher-order type function with a function-typed parameter M<?>
:
type HigherOrderTypeFn<T, M<?>> = T extends (...)
? M<T>
: never;
M<?>
is syntactically incorrect TypeScript, but declaring the type signature as HigherOrderTypeFn<T, M>
yields the error Type 'M' is not generic. ts(2315)
on the second line.
Am I correct assuming that such a type is currently unrepresentable in TS?
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):
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>
(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.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.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>>
.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>;
You're correct, it's not currently representable in TypeScript. There's a longstanding open GitHub feature request, microsoft/TypeScript#1213, which should probably be titled something like "support higher kinded types" but currently has the title "Allow classes to be parametric in other parametric classes".
There are some ideas in the discussion about how to simulate such higher kinded types in the current language (see this comment for a concrete example), but in my opinion they probably don't belong in production code. If you have some specific structure you're tying to implement, maybe something appropriate can be suggested.
But in any case if you want to increase the chance (probably negligibly so, unfortunately) that this will ever happen you might want to go to that issue and give it a 👍 and/or describe your use case if you think it's particularly compelling compared to what's already there. Okay, hope that helps; good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With