Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Applying a function generic from a different context #2

Open
ChuckJonas opened this issue Mar 16, 2023 · 6 comments
Open

Applying a function generic from a different context #2

ChuckJonas opened this issue Mar 16, 2023 · 6 comments

Comments

@ChuckJonas
Copy link

I ended up here from this typescript issue. and think this library might solve the problem I'm running into.

I'm trying to create an library interface where a generic function can be passed in and the type basically resolved from a different context:

Hopefully this Typescript-Playground isn't too hard to follow.

Sorry for such a direct question, please close/delete if this isn't appropriate.

@geoffreytools
Copy link
Owner

geoffreytools commented Mar 17, 2023

Hi,

No problem, I consider it part of the documentation.

People often mention higher kinded types when this use case comes up, but no implementation of HKT in TS can apply a generic function type with an argument dynamically, because the parameters in a generic function are not of the same kind as the parameters in a generic type alias or interface and TS doesn't provide any hook to access them at the type level.

However type-lenses may help if you are ready to take a risk.

First, let me suggest something simpler that we can do at the value level, because it may be enough for your use case.

declare function validate <Request>(req: Request): { validated: Request };

// we can actually do this at the value level
const validateFooString = validate<Paths['/route']['get']['request']>

const api: Api<Paths, typeof validateFooString> = {
  before: validate,
  api: {
    "/route": {
      get: (req, before) => {
        before.validated
         //        ^? (property) validated: { foo: string; }

        return (before as any).validated.foo
      }
    }
  }
}

playground

Now if we want to do this dynamically, an idea is to replace unknown in ReturnType<B> with P[path][method]['request'], which is not what we want semantically but yields the same result in this instance.

type-lenses require a path to replace some piece of type with another. We can write a utility type that looks for unknown in an arbitrarily nested object and generates the path to go there.

// create a path leading to `unknown` in the object type
type CreatePath<T> = FilterPath<GetPaths<T>>

// get all possible paths and the value they lead to in the object type
type GetPaths<T> = T extends object
    ? { [K in keyof T]: [K, ...GetPaths<T[K]>] }[keyof T]
    : [T];

// exclude paths which don't lead to `unknown`
type FilterPath<T> = T extends [unknown, ...unknown[]]
  ? IsUnknown<Last<T>> extends true ? Init<T> : never
  : never;

With this path it is now possible to leverage Replace

// in API
before: Replace<CreatePath<ReturnType<B>>, ReturnType<B>, P[path][method]['request']>

No change needs to be made to api

const api: Api<Paths, typeof validate> = {
  before: validate,
  api: {
    "/route": {
      get: (req, before) => {
        before.validated
        //        ^? (property) validated: { foo: string; }

        return (before as any).validated.foo
      }
    }
  }
}

playground

Now the risk I mentioned is that unknown is a perfectly valid type which could appear multiple times in an object, and not necessarily in places you want to find/replace, so use this with caution!

@ChuckJonas
Copy link
Author

First off, seriously thank you so much for taking the time to write out such a lengthly and complete response! 🙇

let me suggest something simpler that we can do at the value level, because it may be enough for your use case.

Unfortunately this isn't sufficient for my use case. In my attempt to make the example code as simple as possible, I removed some critical context. The goal is to get the before response type to work across various routes, based on the request types of each route:

Playground

It's going to take me a bit to fully grok this, but it definitely seems inline with what I'm looking for!

I do have one immediate followup question....

Why, if I replace the return type of validated with something that doesn't use the generic parameter Response, does the type before becomes the same as request?

I assume it has something to do with the conditional types used here Replace<CreatePath<ReturnType<B>>, ReturnType<B>, P[path][method]['request']> somehow looking for the type of P[path][method]['request'] and falling back to P[path][method]['request'] when it's not found?

Playground

@geoffreytools
Copy link
Owner

You're welcome.

The issue here is that CreatePath<{ validated: boolean }> returns never, because there is no path leading to unknown. The behaviour of Replace is unspecified when no path is provided.

I would compare CreatePath<ReturnType<B>> with never and branch over that

before: CreatePath<ReturnType<B>> extends infer Path extends Query
  ? [Path] extends [never]
    ? ReturnType<B>
    : Replace<Path, ReturnType<B>, P[path][method]['request']>
  : never

Note that you need to import the constraint Query from type-lenses.

playground

This reminds me that when using ReturnType on a generic function, if the generic has a type constraint, it's going to be substituted with the type constraint instead of unknown, and as a result CreatePath won't detect any path either. One way to deal with this could be to look for supertypes of P[path][method]['request']> instead of unknown when generating the path, but as you can imagine the risk of collision with legitimate types becomes very high.

@ChuckJonas
Copy link
Author

Awesome thanks!

I've been messing around with type-lenses for the last hour and starting to get it. Still TBD if I'll have the skills to incorporate this back into the full vision, but such a cool little utility library!

I think I enjoy writing types more than the code itself 😅

@geoffreytools
Copy link
Owner

geoffreytools commented Mar 18, 2023

Yeah that's something to be wary of actually ;)

By the way, this issue made me consider including path finding to type-lenses, and changing the behaviour of Replace when the path is never so that users don't need to branch.

Don't hesitate to post issues if the documentation wasn't very clear. I don't get a lot of feedback.

@geoffreytools
Copy link
Owner

geoffreytools commented Aug 16, 2023

type-lenses now implements FindReplace for this kind of use case, so this:

before: CreatePath<ReturnType<B>> extends infer Path extends Query
  ? [Path] extends [never]
    ? ReturnType<B>
    : Replace<Path, ReturnType<B>, P[path][method]['request']>
  : never

can be re-written as:

before: FindReplace<ReturnType<B>, unknown, [P[path][method]['request']]>

Notice the brackets around P[path][method]['request']. FindReplace expects a tuple of replace values (or a replace callback)

Replace now fails gracefully when the query is never, and a general FindPath utility type was added to the library, so the following would also work:

before: Replace<FindPath<ReturnType<B>, unknown>, ReturnType<B>, P[path][method]['request'], any>

Notice the any at the end. Replace now type checks the replace value against the query, which is problematic when it requires a generic to be resolved (here B). any lets us disable the check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants