H
Hono2mo ago
meow

No Types for RPC with abstracted routes

I'm not getting any type hints from my Hono RPC client. I know there are warnings not to use the factory method, but as far as I knew this was the only way to have abstracted routes, which I find pretty important for organizing many routes.
type Env = {
Variables: {
db: typeof db
mqtt: typeof mqtt
redis: typeof redis
}
}

function initApp(app: Hono<Env, BlankSchema, "/">) {
app.use(async (ctx, next) => {
ctx.set("db", db)
ctx.set("mqtt", mqtt)
ctx.set("redis", redis)
await next()
})
}

const factory = createFactory<Env>({ initApp })
export default factory
// ———————————————————————————————————————————
const app = factory.createApp()
.route("/", root)
.route("/config", config)
.route("/redis", redis)

export type App = typeof app
// ———————————————————————————————————————————
const client = hc<App>(`http://${HOSTNAME}:${PORT}`)
export default client
type Env = {
Variables: {
db: typeof db
mqtt: typeof mqtt
redis: typeof redis
}
}

function initApp(app: Hono<Env, BlankSchema, "/">) {
app.use(async (ctx, next) => {
ctx.set("db", db)
ctx.set("mqtt", mqtt)
ctx.set("redis", redis)
await next()
})
}

const factory = createFactory<Env>({ initApp })
export default factory
// ———————————————————————————————————————————
const app = factory.createApp()
.route("/", root)
.route("/config", config)
.route("/redis", redis)

export type App = typeof app
// ———————————————————————————————————————————
const client = hc<App>(`http://${HOSTNAME}:${PORT}`)
export default client
30 Replies
ambergristle
ambergristle2mo ago
hey! where have you seen warnings not to use the factory method? from the docs, that seems to be the right approach for your goal: https://hono.dev/docs/guides/best-practices#factory-createhandlers-in-hono-factory are you chaining methods in the route apps?
meow
meowOP2mo ago
Are you doing the same pattern? And are you finding that type hinting for the RPC methods work?
ambergristle
ambergristle2mo ago
i haven't used either the factory or rpc extensively
meow
meowOP2mo ago
Yeah I'm chaining my methods, but for whatever reason my types dont' show up for the RPC I'm surprised there isn't a full tutorial or documentation on this pattern
ambergristle
ambergristle2mo ago
so, i reproed your example locally, and the client intellisense is working fine what type inference isn't working as expected?
meow
meowOP2mo ago
Hm, for me the response types for json come back as type never
ambergristle
ambergristle2mo ago
probably because i mocked your routes to repro try starting w a minimal example + scaling up. that should help identify the breaking code is it a specific route/endpoint that's not working, or is it all of them?
meow
meowOP2mo ago
Oh it's a specific endpoint that's screwed up. Let's say I'm doing type A | undefined. Should that come back as type never? Removing undefined by throwing fixes the type problem and will give me back the rich type A I would've expected something like type discrimination where I get to check if a type is undefined at the other point, and if not then it's A
ambergristle
ambergristle2mo ago
it's hard to say from such a generic example it would be helpful if you shared the handler code, along with what you'd like/expect the return type to be but Hono RPC only works with json or text responses, and if you try to mix in a c.body, that will break your typing tl;dr - when you do a union like WhateverType | any, in some cases that will effectively get flattened to any RPC needs a contentful response, type to work, so if you're not returning c.text or c.json, i would expect you to have ts problems https://github.com/honojs/hono/blob/d72aa4b6d77c7b3150bf2b7bae001e6635fe98ae/src/types.test.ts#L2332
meow
meowOP2mo ago
.get("/", async (c) => {
const db = c.get("db")
const empty: Drone = {
id: 0,
id_transaction: "",
id_business: "",
id_device: "",
timestamp: new Date(0),
height: 0,
latitude: 0,
longitude: 0,
pitch: 0,
roll: 0,
yaw: 0,
velocity_xy: 0,
velocity_z: 0,
}
const data = await db
.select()
.from(table.drone)
.orderBy(desc(table.drone.timestamp))
.limit(1)
.then(row => row.at(0)) ?? empty
return c.json(data)
})
.get("/", async (c) => {
const db = c.get("db")
const empty: Drone = {
id: 0,
id_transaction: "",
id_business: "",
id_device: "",
timestamp: new Date(0),
height: 0,
latitude: 0,
longitude: 0,
pitch: 0,
roll: 0,
yaw: 0,
velocity_xy: 0,
velocity_z: 0,
}
const data = await db
.select()
.from(table.drone)
.orderBy(desc(table.drone.timestamp))
.limit(1)
.then(row => row.at(0)) ?? empty
return c.json(data)
})
In this example, the database has a chance of returning undefined. And IMO undefined | T should be the return type without using empty. If I remove the empty logic then it'll be return type never! Which IMO makes very very little sense So now I'm adding extra Hono-specific logic just to appease the type system, which I didn't have to do with Elysia I should be able to discriminate on the other side with if (x !== undefined) {...} Even weirder is that if I return [] or {} that also counts as returning type never! Shouldn't that be never[] and not simply never?
ambergristle
ambergristle2mo ago
interesting. so you're grabbing the first element using at, which i would also expect to yield T | undefined
meow
meowOP2mo ago
Yup, it is Even weirder is that I can simply do return c.json({}) and it'll say that's type never
ambergristle
ambergristle2mo ago
ohhhhhhhh yo
meow
meowOP2mo ago
Did I do something retarded
ambergristle
ambergristle2mo ago
this is invalid, i think: c.json(undefined) try nesting or using null
meow
meowOP2mo ago
??? But empty object is valid JSON... Sometimes you want to return empty array
ambergristle
ambergristle2mo ago
but that's not what row.at(0) returns
// `data` is type Drone | undefined
const data = await db
// ...
.then(row => row.at(0))

// if `data` is undefined, this is invalid
return c.json(data);
// `data` is type Drone | undefined
const data = await db
// ...
.then(row => row.at(0))

// if `data` is undefined, this is invalid
return c.json(data);
meow
meowOP2mo ago
Yup so I do ?? [] or ?? {}
ambergristle
ambergristle2mo ago
if you do this instead:
// `data` is type Drone | undefined
const data = await db
// ...
.then(row => row.at(0))

// just wrap it in an object, and you should be gtg
return c.json({ data });
// `data` is type Drone | undefined
const data = await db
// ...
.then(row => row.at(0))

// just wrap it in an object, and you should be gtg
return c.json({ data });
i'd expect it to work fine
meow
meowOP2mo ago
Ohh I see Okay that works as a workaround Thanks so much
ambergristle
ambergristle2mo ago
or you can use null
meow
meowOP2mo ago
But IMO... c.json({}) shouldn't be never!!! It should just be an empty object!
ambergristle
ambergristle2mo ago
it's not. that's why you get it when you remove empty
meow
meowOP2mo ago
Try it directly. Create an empty route that just does return c.json({})
ambergristle
ambergristle2mo ago
// `data` is type Drone | undefined
const data = await db
// ...
.then(row => row.at(0))
// this is a really common pattern used to deal
// with the fact that `undefined` isn't valid json
const response = data ?? null;
return c.json(response);
// `data` is type Drone | undefined
const data = await db
// ...
.then(row => row.at(0))
// this is a really common pattern used to deal
// with the fact that `undefined` isn't valid json
const response = data ?? null;
return c.json(response);
meow
meowOP2mo ago
.get("/", async (c) => {
return c.json({})
})
.get("/", async (c) => {
return c.json({})
})
I simplified it to just this Your advice works and I'm gonna be doing that But the above route truly does return never And when I inspect the response on the other end it returns an object on the other end since that's valid JSON
ambergristle
ambergristle2mo ago
well i'll be damned
{
json(): F extends "text" ? Promise<never> : F extends "json" ? Promise<BlankRecordToNever<T>> : Promise<unknown>;
}
{
json(): F extends "text" ? Promise<never> : F extends "json" ? Promise<BlankRecordToNever<T>> : Promise<unknown>;
}
meow
meowOP2mo ago
Ah hmm
ambergristle
ambergristle2mo ago
so it's definitely intentional. i wonder why tho
meow
meowOP2mo ago
They specifically ban blank objects, I see

Did you find this page helpful?