[zod-openapi] `tsc` error TS2345 when using a `query` input data
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/[email protected]
├── @sinclair/[email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
└── [email protected]
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;
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().
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 ?
Not sure I follow, doesn't
coerceaim to treat the query as astringand convert it by itself?
Ideally, it should do it, but not. Also the input type will be number.
If I understand correctly, you propose to first declare as
z.string()and usetransformfor parsing to a number. How would that be different fromcoerce?
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
}),
})
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.
Hi,
Maybe one last question then: in what context would you use
coercewithout 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.
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.