poteat / hkt-toolbelt

✨Functional and composable type utilities

Home Page:http://hkt.code.lol

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.