How to not break type inference with middleware?

Hey eveyone, when doing something like this:
const orgsRoutes = new Hono<AuthedContext>()
.post('/', zValidator('json', createOrgFormSchema), async (c) => {
// create org logic

return c.json({ organizationId: newOrg.id }, 201);
})
.use('/:orgId/*', middleware)
.get('/:orgId', async (c) => {
// ...
})
const orgsRoutes = new Hono<AuthedContext>()
.post('/', zValidator('json', createOrgFormSchema), async (c) => {
// create org logic

return c.json({ organizationId: newOrg.id }, 201);
})
.use('/:orgId/*', middleware)
.get('/:orgId', async (c) => {
// ...
})
The Hono RPC client breaks for the POST / route.
Property '$post' does not exist on type '{ ":orgId": { "*": ClientRequest<{ $post: { input: { json: { name: string; }; }; output: { organizationId: string; }; outputFormat: "json"; status: 200; } | { input: { json: { name: string; }; }; output: { organizationId: string; }; outputFormat: "json"; status: 201; }; }>; }; } & ... 9 more ... & { ...;
How to fix this?
17 Replies
ambergristle
ambergristle•4w ago
it's probably client.index.$post, no?
Franco Romano Losada
Franco Romano LosadaOP•4w ago
I don't have that option, just :orgId
No description
ambergristle
ambergristle•4w ago
interesting. so you don't have api.orgs.index.$post? your code looks fine, so i'm not sure what the issue is i can't repro an issue with your snippet locally. it could have something to do with the create org logic, but that seems like a stretch i'd recommend trying to get a simple reproducible example working (or not)
Franco Romano Losada
Franco Romano LosadaOP•4w ago
No, and if I comment the .use block I get api.orgs.$post, not api.orgs.index.$post Can you try this?
const testRoutes = new Hono<AppContext>()
.get('/', async (c) => {
return c.json({ message: 'Hello, world!' });
})
.post('/', async (c) => {
return c.json({ message: 'Hello, world!' });
})
.use('/:testId', async (c, next) => {
return next();
})
.get('/:testId', async (c) => {
return c.json({ message: 'Hello, world!' });
});

const app = new Hono<AppContext>()
.route('/test', testRoutes)

const api = hc<App>(env.PUBLIC_API_URL) // replace with localhost or whatever, doesn't matter its just for the types
const testRoutes = new Hono<AppContext>()
.get('/', async (c) => {
return c.json({ message: 'Hello, world!' });
})
.post('/', async (c) => {
return c.json({ message: 'Hello, world!' });
})
.use('/:testId', async (c, next) => {
return next();
})
.get('/:testId', async (c) => {
return c.json({ message: 'Hello, world!' });
});

const app = new Hono<AppContext>()
.route('/test', testRoutes)

const api = hc<App>(env.PUBLIC_API_URL) // replace with localhost or whatever, doesn't matter its just for the types
if you do api.test. the auto-complete should only type :testId
ambergristle
ambergristle•4w ago
hm. i see. that's kind of strange there are a few short-term fixes - use a sub-route for /:testId - move the /:testId middleware to the top of the chain - leave the /:testId middleware where it is, but remove the path. i'm pretty sure that middleware doesn't get applied to routes it's chained after. you'd need to double-check though i'll take a closer look at the typing when i have a sec. it's probably just an artifact of hono's type system, but it's a little strange that middleware is totally breaking the chain @Franco Romano Losada one call-out though: you should never return next from hono middleware just await it it should be fine from a technical perspective, but there's no real advantage, and it introduces unneded complexity to the request flow, which can make debugging harder
Franco Romano Losada
Franco Romano LosadaOP•4w ago
I feel quite stupid because I tried so many ways but not to move the middleware to the top of the chain. Thanks a lot Removing the path was one of the solutions BUT then c.req.param('orgId') doesn't work because it needs the path. I wish hono exposes the internal function in the hono/route package.
ambergristle
ambergristle•4w ago
no problem! we've all been there, lol
Franco Romano Losada
Franco Romano LosadaOP•4w ago
Noted!
ambergristle
ambergristle•4w ago
i'm not sure i follow this.
const app = new Hono()
.get('/', /** */)
.use(/** */) // <- i think this will only apply to routes after it
.get('/:id', /** */) // <- this will still have access to /:id
const app = new Hono()
.get('/', /** */)
.use(/** */) // <- i think this will only apply to routes after it
.get('/:id', /** */) // <- this will still have access to /:id
or do you need the path param in the middleware itself?
Franco Romano Losada
Franco Romano LosadaOP•4w ago
Yes, exactly, I need the path param in the middleware
ambergristle
ambergristle•4w ago
ngl, this typing thing is pretty strange. i'll need to take a closer look when i have a bit more time
Franco Romano Losada
Franco Romano LosadaOP•4w ago
can I help you? can you point where the type generation is happening in hono source code?
ambergristle
ambergristle•3w ago
you're welcome to poke around. i'll let you know if i come up with any hypotheses this is the core typing: https://github.com/honojs/hono/blob/fa8eef707990e80a593fa32c2f67713a2ecd2e9b/src/types.ts#L73 but this may also be relevant: https://github.com/honojs/hono/blob/main/src/client/types.ts right now i'm thinking it has to do with how hono merges types together, which i think happens in the client typing, but it's been a while so i'd probably start by creating a pretty simple set of routes, and just mess around with the typing to see what different levers do, and how middleware affects typing and maybe copy/paste some of hono's types into my own project, to see if i can recreate the same type logic manually @Franco Romano Losada so i finally had a chance to look into this turns out its explicit/intentional behavior: https://github.com/honojs/hono/blob/971106d132ec8a989be12ec5c8e63cfaf597cd4f/src/hono-base.ts#L152 using app.use('/path', middleware) updates the Hono path to take a step back, the typing is correct, in that it's consistent with the actual use method behavior sorry, i got a little confused. that doesn't actually explain this issue i've narrowed it down to this though
type ChangePathOfSchema<S extends Schema, Path extends string> = keyof S extends never ? {
[K in Path]: {};
} : ({
[K in keyof S as Path]: S[K];
});
type ChangePathOfSchema<S extends Schema, Path extends string> = keyof S extends never ? {
[K in Path]: {};
} : ({
[K in keyof S as Path]: S[K];
});
ok. so this is a bug, but it's an unintendent consequence of a different bug fix here's the case it's supporting:
new Hono()
.use('/path', /** */)
.get((c) => /** */) // actually at `GET /path`
new Hono()
.use('/path', /** */)
.get((c) => /** */) // actually at `GET /path`
calling methods with a path argument sets the Hono internal _path property, which is then used if the immediate downstream method doesn't have a path (otherwise it defaults to basePath, i guess) anyways, hono's type system does some type merging behind the scenes to resolve things correctly, and this breaks down when you start to venture into these sorts of edge cases the way that the internal _path gets typed on the instance is also involved idk. i'll open an issue with my findings so someone with more ts experience (or knowledge of the system) can take a crack at it
ambergristle
ambergristle•3w ago
GitHub
Downstream methods without path args break upstream method path t...
What version of Hono are you using? 4.10.5 What runtime/platform is your app running on? (with version if possible) Bun 1.2.13 What steps can reproduce the bug? a typing edge case was reported on d...
Franco Romano Losada
Franco Romano LosadaOP•3w ago
Hi mate, I'm really sorry I dissapeared and couldn't help you debug this. I had some of the stressful days of the last years Thanks a lot for debugging it and creating the issue
ambergristle
ambergristle•3w ago
no worries! i just wanted to satisfy my curiosity. hope you're feeling better!
Franco Romano Losada
Franco Romano LosadaOP•3w ago
Thank you 🙂

Did you find this page helpful?