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

Is there way to create wrapper function for route handler inferring schema type? #97

Open
2 tasks done
benevbright opened this issue Aug 12, 2023 · 5 comments
Open
2 tasks done

Comments

@benevbright
Copy link

benevbright commented Aug 12, 2023

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the issue has not already been raised

Issue

This is more about TS question... sorry about asking it here.

I have a simple Fastify route. my fastify is using fastify.withTypeProvider<TypeBoxTypeProvider>();

    fastify.post(
      'v1/article',
      {
        schema: {...},
      },
      async (request, reply) => {
        // everything is good and `request` has inferred schema type.
        reply.send("ok");
      }
    );

I want to create a wrapper for the route handler

    fastify.post(
      'v1/article',
      {
        schema: {...},
      },
      myWrapper(async (request, reply) => {
        // type is lost and reply becomes unknown!
       // and I can't infer schema type here
        reply.send("ok");
      })
    );

This looks simple thing to do but I failed to pass inline function's type to my wrapper's parameter.

I've tried something like below

const myWrapper = <RQ, RP>(handler: (req: RQ, rep: RP) => Promise<void>) => {
  return async (req: RQ, rep: RP) => {
    await handler(req, rep);
  };
};

or

const myWrapper = <T, A extends unknown[]>(handler: (...args: A) => T) => {
  return handler;
};

But this doesn't work and not passing inline function's type to my wrapper's parameter function... I know what's wrong here but is there any way to solve my issue?

(I can't use types provided from Fastify because I need type inferring from the schema)

Thanks

@benevbright
Copy link
Author

@sinclairzx81 Hi, I'm wondering if you can take a look if that's possible with typescript? Thanks.

@sinclairzx81
Copy link
Contributor

sinclairzx81 commented Aug 13, 2023

@benevbright Hi!

I had a quick look. Unfortunately, I don't think this pattern will be possible with Type Providers as the request context (for request and response types) don't have any way to propagate into the wrapper when assigning functions that way.

The following are 3 implementations of this pattern, ranging from explicit function calls to closure assignment (as per your example). This just to show where things go wrong.

Wrapper Function

Here's the wrapper function used on all 3 examples.

type Callback<Request, Response> = (request: Request, response: Response) => any

function wrapper<
   Request = unknown, 
   Response = unknown
>(callback: Callback<Request, Response>): Callback<Request, Response> {
  // todo: implement wrapper stuff here ...
  return (request, response) => callback(request, response)
}

Form 1

Explicit unwrap calls in the handler, specify req/res types via generics. The following works fine (albeit verbose)

fastify.post('/', {
  schema: {
    body: Type.Number(),
    response: {  200: Type.Number() } }
}, (req, res) => {
  const closure = wrapper<typeof req, typeof res>((req, res) => { 
    // handle request
  })
  closure(req, res) // important! For this call to be type safe, you need
                    // the signature derived via generics (see typeof)
})

Form 2

Reduce call to inline closure, retain generics, return any to avoid route return type expectations. This also works fine, however we use any as we're unable to have the exterior wrapper return a closure matching the routes expected return type.

fastify.post('/', {
  schema: {
    body: Type.Number(),
    response: { 
      200: Type.Number()
    }
  }
}, (req, res) => wrapper<typeof req, typeof res>((req, res) => {
  
  // handle request

}) as any) // to get around issues resolving the route handler return type

Form 3

Reduce to expected wrapper pattern. The following will type check, however we've lost the inference for req and res as the route parameters cannot be derived (refer to Form 1 closure invoke !important comment)

fastify.post('/', {
  schema: {
    body: Type.Number(),
    response: { 
      200: Type.Number()
    }
  }
}, wrapper((req, res) => { // req, res are both unknown

  // handle request

}) as any)

So, based on the above, you can kind of see where things are breaking down, first the route return type, then the req, res inference. Unfortunately, there's not really a trivial way to ensure types propagate correctly without the req, res parameters. There may be something you can do with creating a mapping FastifyRouteHandler type (and deriving req, res through conditional mapping), but would likely run into invariant type issues along the way (and overall the implementation of the type would be quite complex). The general takeaway I guess is that wrapper patterns are generally not supported with Type Providers (with Form 2 about the best you can reduce things currently)

Hope this helps
S

@benevbright
Copy link
Author

benevbright commented Aug 13, 2023

hi @sinclairzx81, thanks a lot for the great answer, always. 🙏👍 It makes sense. As you said, it seems when TS goes deep into to infer T, it hits the wall with complex type of Fastify router, which is very hard to resolve and maintain with my example pattern.

@kevbook
Copy link

kevbook commented Sep 28, 2023

@benevbright - have you come up with something yet? I am trying to build a tiny RPC-style wrapper and running into this same issue

@benevbright
Copy link
Author

@kevbook unfortunately I dropped it 🙏

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

3 participants