Idea: Experiment / document generic lambda types further with HKTs. (e.g. `<X>(x: X) => X`)
daniel-nagy opened this issue · comments
Hi,
This is not an issue, so feel free to close, but I had a question about mapping over a function's input and output at the type level, specifically for generic functions.
It seems that it is not possible to map over a function's input and output types while propagating type parameters, using this trick, and I was wondering if you had thought about this use case at all. For example, consider a HKT that takes a function and returns the same function except the output is wrapped in a Promise.
import { $, Function, Kind, Type } from "hkt-toolbelt";
interface ReturnsPromise extends Kind.Kind {
f(
x: Type._$cast<this[Kind._], Function.Function>
): typeof x extends (...a: infer A) => infer R
? (...a: A) => Promise<Awaited<R>>
: never;
}
type Test = $<ReturnsPromise, <U>(a: string, b: U) => 12>
In this example the type parameters will be inferred as unknown
. This is possible at the value level.
declare function returnsPromise<A extends unknown[], R>(x: (...a: A) => R): (...a: A) => Promise<Awaited<R>>
const test = returnsPromise(<U>(a: string, b: U) => 12);
Thanks for taking the time to entertain my question, this is a very interesting concept!
I totally don't mind answering this. It seems you are making the assertion that a certain HKT cannot be constructed. The HKT you are interested in is an HKT which has the following behavior:
$<Promisify, () => void> // () => Promise<void>
$<Promisify, (x: number) => number> // (x: number) => Promise<number>
To my knowledge this is trivially accomplished via first defining _$promisify
, and then currying it with the standard HKT approach. I'll try implementing it myself and respond separately on the result.
This is indeed trivial as implemented here: https://tsplay.dev/wOXL7N
Posting the actual block as well to avoid bit rot.
import * as H from "hkt-toolbelt";
type _$promisify<T> = T extends (...args: infer Args) => infer R ? (...args: Args) => Promise<R> : never;
interface Promisify extends H.Kind.Kind { f(x: this[H.Kind._]): _$promisify<typeof x> }
You should see that this representation works fine. You don't even need a separate _$promisify
, this is just something I do in this repo to keep things organized and efficient.
This also works:
import * as H from "hkt-toolbelt";
interface Promisify extends H.Kind.Kind {
f(x: this[H.Kind._]): typeof x extends (...args: infer Args) => infer R ? (...args: Args) => Promise<R> : never;
}
I realize this doesn't implement the full complexity of your code above; indeed, the reason why your code doesn't work is because you are passing in a generic function type <U>(a: string, b: U) => 12
. There are indeed limitations when working with these types. To my knowledge, as soon as you "loop" over these parameters, the U
will be collapsed to unknown
. I would say that we should have tests for these types and also document what happens when you pass them in to certain utilities.