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

[zod-openapi] tsc error TS2345 when using a query input data #200

Open
0237h opened this issue Oct 15, 2023 · 6 comments
Open

[zod-openapi] tsc error TS2345 when using a query input data #200

0237h opened this issue Oct 15, 2023 · 6 comments

Comments

@0237h
Copy link

0237h commented Oct 15, 2023

Hi team,
I encountered this issue when trying to build an API with more than one input type (e.g. PATH + QUERY like /{path}?q=xxx).

When using zod-openapi with a route including query input data with (or without) params, the type resolution fails when trying to access the input from the handler.

In this context (the sample file is provided at the end of this post)

app.openapi(route, async (c) => {
    const { paramValue } = c.req.valid('param') as ParamSchema;
    const { queryValue } = c.req.valid('query') as QuerySchema;
    ...
});

the calls to c.req.valid will produce the following errors at compile time:

src/min.ts:43:40 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.                                                 

43     const { paramValue } = c.req.valid('param') as ParamSchema;
                                          ~~~~~~~                           

src/min.ts:44:40 - error [tsc.trace.both.gz](https://github.com/honojs/middleware/files/12908484/tsc.trace.both.gz): Argument of type 'string' is not assignable to parameter of type 'never'.

44     const { queryValue } = c.req.valid('query') as QuerySchema;
                                          ~~~~~~~                   


Found 2 errors in the same file, starting at: src/min.ts:43

that is the output of bun run tsc --noEmit --pretty --skipLibCheck --strict src/min.ts

Note that the problem disappears if we just use the param as input:

const route = createRoute({
    method: 'get',
    path: '/{paramValue}/path',
    request: {
        params: ParamSchema,
        // query: QuerySchema,
    },
    ...
});

app.openapi(route, async (c) => {
    const { paramValue } = c.req.valid('param') as ParamSchema; // Works !
    ...
});

But not when commenting out the param and leaving just the query:

const route = createRoute({
    method: 'get',
    path: '/path', // Note the {param} is removed from the path
    request: {
        //params: ParamSchema,
        query: QuerySchema,
    },
    ...
});

const app = new OpenAPIHono();

app.openapi(route, async (c) => {
    // error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
    const { queryValue } = c.req.valid('query') as QuerySchema;
    ...
});

Build traces

Looking at the build traces from tsc, we can see for param the display type is correct <T extends \"param\">(target: T) ... allowing to proceed without any errors.

Param only

{"id":15461,"symbolName":"valid","recursionId":3796,"firstDeclaration":{"path":"/home/user/Documents/substreams-clock-api/node_modules/hono/dist/types/request.d.ts","start":{"line":57,"character":71},"end":{"line":58,"character":94}},"flags":["Object"],"display":"<T extends \"param\">(target: T) => InputToDataByTarget<{ param: { paramValue: \"a\" | \"b\" | \"c\"; }; }, T>"},

However in the other cases, it resolves to <T extends never>(target: T) ... making it impossible to infer the type.

Query only

{"id":15453,"symbolName":"valid","recursionId":3793,"firstDeclaration":{"path":"/home/user/Documents/substreams-clock-api/node_modules/hono/dist/types/request.d.ts","start":{"line":57,"character":71},"end":{"line":58,"character":94}},"flags":["Object"],"display":"<T extends never>(target: T) => InputToDataByTarget<undefined, T> | InputToDataByTarget<Partial<{ json: unknown; form: unknown; query: unknown; queries: unknown; param: unknown; header: unknown; cookie: unknown; }>, T>"},

Both

{"id":15472,"symbolName":"valid","recursionId":3803,"firstDeclaration":{"path":"/home/user/Documents/substreams-clock-api/node_modules/hono/dist/types/request.d.ts","start":{"line":57,"character":71},"end":{"line":58,"character":94}},"flags":["Object"],"display":"<T extends never>(target: T) => InputToDataByTarget<undefined, T> | InputToDataByTarget<Partial<{ json: unknown; form: unknown; query: unknown; queries: unknown; param: unknown; header: unknown; cookie: unknown; }>, T>"},

The full traces for each are available here: tsc.traces.tar.gz

Related

In #77, the problem appeared to be fixed by using strict: true for the config and upgrading to the latest versions of hono and @hono/zod-validator. This doesn't seem to fix the problem in this case.

System information

Bun version 1.0.2

bun pm ls

node_modules (16)
├── @hono/zod-openapi@0.7.2
├── @sinclair/typebox@0.31.17
├── bun-types@0.6.14
├── dotenv@16.3.1
├── hono@3.7.6
├── typescript@5.2.2
└── zod@3.22.4

Sample file

import { OpenAPIHono, z, createRoute } from '@hono/zod-openapi';
import { TypedResponse } from 'hono';

const ParamSchema = z.object({
    paramValue: z.enum(['a', 'b', 'c'])
    .openapi({
        param: {
            name: 'paramValue',
            in: 'path',
        }
    })
});
type ParamSchema = z.infer<typeof ParamSchema>;

const QuerySchema = z.object({
    queryValue: z.coerce.number()
    .openapi({
        param: {
            name: 'queryValue',
            in: 'query',
        }
    })
});
type QuerySchema = z.infer<typeof QuerySchema>;

const route = createRoute({
    method: 'get',
    path: '/{paramValue}/path',
    request: {
        params: ParamSchema,
        query: QuerySchema,
    },
    responses: {
        200: {
            description: 'Sample endpoint',
        },
    },
});

const app = new OpenAPIHono();

app.openapi(route, async (c) => {
    const { paramValue } = c.req.valid('param') as ParamSchema;
    const { queryValue } = c.req.valid('query') as QuerySchema;

    return {
        response: c.text("Not working...")
    } as TypedResponse<string>;
});

export default app;
0237h added a commit to pinax-network/substreams-clock-api that referenced this issue Oct 15, 2023
Leveraging the schemas defined using `zod` we can infer the type of most
of the input / output types for the API. There's a pending issue
concerning the type inference of `query` parameters that is stuck to
`never` currently (see honojs/middleware#200).
0237h added a commit to pinax-network/substreams-clock-api that referenced this issue Oct 15, 2023
* Fix typing issues from `tsc`

Leveraging the schemas defined using `zod` we can infer the type of most
of the input / output types for the API. There's a pending issue
concerning the type inference of `query` parameters that is stuck to
`never` currently (see honojs/middleware#200).

* Refactor env schema validation to use `zod`

* Fix tests
@yusukebe
Copy link
Member

Hi @Krow10,

Apologies for the delayed response.

We can only use the string type for a query, so you won't be able to use z.coerce.number(). Instead, you could use something like transform().

@0237h
Copy link
Author

0237h commented Oct 20, 2023

Hi @yusukebe, no worries thanks for the response.

Not sure I follow, doesn't coerce aim to treat the query as string and convert it by itself ?
If I understand correctly, you propose to declare first as z.string() and use transform for parsing to a number how would that be different than coerce ?

@yusukebe
Copy link
Member

Not sure I follow, doesn't coerce aim to treat the query as a string and convert it by itself?

Ideally, it should do it, but not. Also the input type will be number.

Screenshot 2023-10-21 at 19 15 39 Screenshot 2023-10-21 at 19 15 44

If I understand correctly, you propose to first declare as z.string() and use transform for parsing to a number. How would that be different from coerce?

Yes. For example, you can write as follows:

const schema = z.object({
  numericValue: z.string().transform((val) => {
    const parsed = parseInt(val)
    if (isNaN(parsed)) {
      return z.NEVER
    }
    // validate `parsed`
    // ...
    return parsed
  }),
})

@0237h
Copy link
Author

0237h commented Oct 27, 2023

A bit late to respond !

Thank you for the example, looks like the proper way to parse it then. Maybe one last question then: in what context would you use coerce without the need for additional transforms ?

The issue can probably be closed now, thanks again.

@yusukebe
Copy link
Member

Hi,

Maybe one last question then: in what context would you use coerce without the need for additional transforms?

coerce in Zod is used when we want to coerce value types, as the naming suggests.

In the case of Zod OpenAPI or Zod Validator of Hono, there isn't a chance to use coerce. Using coerce implies that the output type is explicit, e.g., String or Number, but it does not know the type of the "input".

In Zod OpenAPI or Zod Validator, the input value of a query should be string because the values are always string, although JSON can have number or boolean values. So, if you use coerce, it does not know the input type, and therefore it throws an error.

@jdavidferreira
Copy link

jdavidferreira commented Nov 21, 2023

I have few experience with zod. I managed to do validate the string query param as a number and also parse it as a number this way:

z.object({
    queryValue: z.string().default('10').pipe(z.coerce.number()),
});

That way, zValidator doesn't complain.

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