fastify-type-provider-typebox icon indicating copy to clipboard operation
fastify-type-provider-typebox copied to clipboard

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

Open benevbright opened this issue 2 years ago • 5 comments

Prerequisites

  • [X] I have written a descriptive issue title
  • [X] 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 avatar Aug 12 '23 10:08 benevbright

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

benevbright avatar Aug 12 '23 10:08 benevbright

@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

sinclairzx81 avatar Aug 13 '23 11:08 sinclairzx81

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.

benevbright avatar Aug 13 '23 20:08 benevbright

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

kevbook avatar Sep 28 '23 22:09 kevbook

@kevbook unfortunately I dropped it 🙏

benevbright avatar Sep 28 '23 22:09 benevbright