'Higher-order type functions in TypeScript?
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?
Solution 1:[1]
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!
Solution 2:[2]
For you and others searching for a workaround, you can try a simple idea based on placeholders (see this comment in the discussion mentioned by jcalz):
type Placeholder = {'aUniqueKey': unknown};
type Replace<T, X, Y> = {
[k in keyof T]: T[k] extends X ? Y : T[k];
};
So, your function would look like follows:
type HigherOrderTypeFn<T, M> = T extends (...) ? Replace<M, Placeholder, T> : never;
and be called for example like that:
type M<U> = U[];
type X = HigherOrderTypeFn<number, M<Placeholder>> // is number[] (if ... is number)
Solution 3:[3]
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 becomesHkt.Output<(typeof FooSet)["hkt"], number>. The magic now involves turning this into aFooSet<number> - First we resolve
(typeof FooSet)["hkt"]toFooSetHkt. A lot of the magic lies here, by storing the info about how to create aFooSetin the statichktproperty ofFooSet. You need to do this for each supported class. - Now we have
Hkt.Output<FooSetHkt, number>. Resolving theHkt.Outputtype alias, we get(FooSetHkt & { [Hkt.input]: number })[typeof Hkt.output]. The unique symbolsHkt.input/Hkt.outputhelp for creating unique properties, but we could have also used unique string constants. - Now we need to access the
Hkt.outputproperty ofFooSetHkt. This is different for each class and contains the details on how to construct a concrete type with the type argument.FooSetHktdefines the output property to be of typeFooSet<Hkt.Input<this>>. - Finally,
Hkt.Input<this>just acceses theHkt.inputproperty ofFooSetHkt. It would resolve tounknown, but by using the intersectionFooSetHkt & { [Hkt.input]: number }, we can change theHkt.inputproperty tonumber. And so if we've reached our goal,Hkt.Input<this>resolves tonumberandFooSet<Hkt.Input<this>>resolves toFooSet<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>;
Solution 4:[4]
There's an implementation of HKTs (leveraging module augmentation) in fp-ts.
The workaround for Higher Kinded Types as documented here by its author is:
export interface HKT<URI, A> {
readonly _URI: URI;
readonly _A: A;
}
And can be used like this:
export interface Foldable<F> {
readonly URI: F;
reduce: <A, B>(fa: HKT<F, A>, b: B, f: (b: B, a: A) => B) => B;
}
Check it out this question: higher kinded type in typescript from fp-ts and URI
Maybe that can shed some light?
Cheers
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | jcalz |
| Solution 2 | Remirror |
| Solution 3 | blutorange |
| Solution 4 |
