openapi-backend icon indicating copy to clipboard operation
openapi-backend copied to clipboard

Default type for Context and Handler prevents extension or override

Open lostrouter opened this issue 1 year ago • 3 comments

if i define a handler like so, and attempt to register it with other handlers, tsc will throw errors.

export const myHandler: Handler<
    unknown,
    { id: string }
> = async (
    c: OpenAPIBackendContext<unknown, { id: string }>,
    ctx: KoaContext,
): Promise<void> => {}
export const v1InternalAPI = new OpenAPIBackend({
    apiRoot: '/v1',
    definition: __dirname + '../../../api/service.v1.internal.yaml',
    // this is needed to honor the "format" field in the openapi definition
    customizeAjv: (originalAjv) => {
        addFormats(originalAjv);
        return originalAjv;
    },
});

// register your framework specific request handlers here
v1InternalAPI.register({
    postResponseHandler,
    validationFail,
    notFound,
    myHandler,
});

the errors

src/routes/v1/router.ts:27:5 - error TS2322: Type 'Handler<unknown, { id: string; }>' is not assignable to type 'Handler<any, UnknownParams, UnknownParams, UnknownParams, UnknownParams, Document>'.
  Property 'id' is missing in type 'UnknownParams' but required in type '{ id: string; }'.

27     myHandler,
       ~~~~~~~~~~~~~~~~~~~~~~~~~~~

  src/controllers/myHandler.ts:12:7
    12     { id: string }
             ~~
    'id' is declared here.
  node_modules/openapi-backend/backend.d.ts:156:9
    156         [operationId: string]: Handler;
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    The expected type comes from this index signature.


Found 1 error in src/routes/v1/router.ts:27

what am i doing wrong here?

lostrouter avatar May 26 '23 01:05 lostrouter

Handler must have 3 parameters: context, Request,Response. You can send your own parameter after those ones.

f.e. async handler(ctx: Context, req: Request, res: Response, myObject: {id: string}, mySecondObject: ...)

kmvpvl avatar May 26 '23 08:05 kmvpvl

That would be true if i were using express. my project is using koa, which only has 2 arguments going into the handler.

lostrouter avatar Jul 18 '23 23:07 lostrouter

src/routes/v1/router.ts:27:5 - error TS2322: Type 'Handler<unknown, { id: string; }>' is not assignable to type 'Handler<any, UnknownParams, UnknownParams, UnknownParams, UnknownParams, Document>'. Property 'id' is missing in type 'UnknownParams' but required in type '{ id: string; }'.

The error you are getting indicates that there's some kind of disjunction in typing since your custom { id: string} parameter does not contradict the Params generic expected by the Handler:

type UnknownParams = {
    [key: string]: string | string[];
}

export type Handler<
  RequestBody = any,
  Params = UnknownParams,
  Query = UnknownParams,
  Headers = UnknownParams,
  Cookies = UnknownParams,
  D extends Document = Document,
> = (context: Context<RequestBody, Params, Query, Headers, Cookies, D>, ...args: any[]) => any | Promise<any>;

Looking at the register method, the current signature suggests that Handler receives no generics at all:

public register(handlers: { [operationId: string]: Handler }): void;

Your custom Handler signature defines an object of a particular shape { id: string }, and while it does fit into the expected generic type { [key: string]: string | string[] }, the opposite is not the case.

It seems to me that we could overcome this issue by making the Handler in the register function aware of each handler's custom generics. The problems is that the registration is performed in bulk and I'm not 100% sure how to dynamically define generics for each handler in this case. On the other hand, doing so individually with the registerHandler method should be more or less straightforward:

registerHandler<
  RequestBody = any,
  Params = UnknownParams,
  Query = UnknownParams,
  Headers = UnknownParams,
  Cookies = UnknownParams,
  D extends Document = Document
>(
  operationId: string,
  handler: Handler<
    RequestBody = any,
    Params = UnknownParams,
    Query = UnknownParams,
    Headers = UnknownParams,
    Cookies = UnknownParams,
    D extends Document = Document
  >
): void;

Circling back to the example provided in the description:

type Body = unknown
type PathParams = { id: string }

const myHandler: Handler<Body, PathParams> = (
  context,
  ctx: KoaContext,
) => {}

api.registerHandler<Body, PathParams >('myHandler', myHandler)

Doing so connects the dots for TypeScript and will prevent type mismatch complaints.

@anttiviljami curios to hear your thoughts on the matter

Powell-v2 avatar Nov 03 '23 11:11 Powell-v2