-
-
Notifications
You must be signed in to change notification settings - Fork 584
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
feat(client): improve handling status code types #3292
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #3292 +/- ##
========================================
Coverage 96.20% 96.20%
========================================
Files 151 151
Lines 15293 15293
Branches 2761 2660 -101
========================================
Hits 14713 14713
Misses 580 580 ☔ View full report in Codecov by Sentry. |
Ok, I can see that later! |
The idea seems good to me, |
generic typed However, number type import { Hono } from './src/'
import { hc } from './src/client'
const app = new Hono()
.get('foo/:id', c => {
if (c.req.param('id') === '1') {
return c.json({ res: 400 as const }, 400)
}
return c.json({ res: 200 as const }, 200)
})
const client = hc<typeof app>('')
client.foo[':id'].$get({ param: { id: '1' }}).then(async res => {
switch (res.status) {
case 200:
return await res.json()
case 400:
return await res.json()
}
}) Second, projects using @typescript-eslint/switch-exhaustiveness-check will be broken. You need to add As @NamesMT said, a default unknown status type possibly works |
I pushed a sample for the Also, now that we have a
// server
import { HTTPException } from './http-exception'
import { Hono } from './hono'
const app = new Hono()
const flag = {}
const routes = app.get('/foo', (c) => {
if (flag) {
throw new HTTPException(500, {
message: 'Server Error!',
})
}
return c.json({ ok: true }, 200)
})
// client
import { hc } from './client'
type HCDefaultResponse = (
{
code: 'unknownErr'
message: string
detail: any
} |
{
code: 'validationErr'
message: string
detail: string[][]
}
)
const client = hc<typeof routes, HCDefaultResponse>('/') // Remove `HCDefaultResponse` to test the opt-in of route's arbitrary status response.
async function demo() {
const res = await client.foo.$get()
switch (res.status) {
case 200:
return await res.json().then(data => data.ok)
default: {
const data = await res.json() // Should be typeof `HCDefaultResponse`
console.error(data.code === 'validationErr'
? { message: data.message, errorCount: data.detail.length }
: { message: data.message })
throw new Error(data.code)
}
}
} |
As you said, we should support the default response type. I like @NamesMT 's implementation. My proposal is to pass the default type generics into const res = await client.foo.$get<DefaultResponseInFoo>() How about this? It includes diff for styling, but we can implement it with the following (diff from main...NamesMT:hono:feat/client-status-code-types_default_opt-in): diff --git a/src/client/client.ts b/src/client/client.ts
index 13641299..512e7675 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -134,17 +134,16 @@ class ClientRequestImpl {
}
}
-
/**
* @param baseUrl The base URL of the Hono instance.
* @param options The options for the client.
* @returns A hono API client instance.
- *
+ *
* **Typedoc**: <Hono, DefaultResponse>
- *
+ *
* - Hono: The type of the Hono instance.
- *
- * - DefaultResponse: This is used in cases where the response is unknown,
+ *
+ * - DefaultResponse: This is used in cases where the response is unknown,
* e.g. checking for an unknown status code:
* ```ts
* // The response of status code 400 is processed by a middleware and not explicitly defined in the router type flow.
@@ -153,7 +152,7 @@ class ClientRequestImpl {
* }
* ```
*/
-export const hc = <H extends Hono<any, any, any>, DefaultResponse = {}>(
+export const hc = <H extends Hono<any, any, any>>(
baseUrl: string,
options?: ClientRequestOptions
) =>
@@ -229,4 +228,4 @@ export const hc = <H extends Hono<any, any, any>, DefaultResponse = {}>(
return req.fetch(opts.args[0], args)
}
return req
- }, []) as UnionToIntersection<Client<H, DefaultResponse>>
+ }, []) as UnionToIntersection<Client<H>>
diff --git a/src/client/types.ts b/src/client/types.ts
index ef54e1fe..664fb4a5 100644
--- a/src/client/types.ts
+++ b/src/client/types.ts
@@ -25,12 +25,18 @@ export type ClientRequestOptions<T = unknown> = {
headers: T | (() => T | Promise<T>)
})
-export type ClientRequest<S extends Schema, DefaultResponse = {}> = {
+export type ClientRequest<S extends Schema> = {
[M in keyof S]: S[M] extends Endpoint & { input: infer R }
? R extends object
? HasRequiredKeys<R> extends true
- ? (args: R, options?: ClientRequestOptions) => Promise<ClientResponseOfEndpoint<S[M], DefaultResponse>>
- : (args?: R, options?: ClientRequestOptions) => Promise<ClientResponseOfEndpoint<S[M], DefaultResponse>>
+ ? <DefaultResponse = {}>(
+ args: R,
+ options?: ClientRequestOptions
+ ) => Promise<ClientResponseOfEndpoint<S[M], DefaultResponse>>
+ : <DefaultResponse = {}>(
+ args?: R,
+ options?: ClientRequestOptions
+ ) => Promise<ClientResponseOfEndpoint<S[M], DefaultResponse>>
: never
: never
} & {
@@ -66,8 +72,8 @@ type ClientResponseOfEndpoint<T extends Endpoint = Endpoint, DefaultResponse = {
? F extends 'redirect'
? ClientResponse<O, S extends RedirectStatusCode ? S : never, 'redirect'>
: Equal<DefaultResponse, {}> extends true
- ? ClientResponse<O, S extends StatusCode ? S : never, F extends ResponseFormat ? F : never>
- :
+ ? ClientResponse<O, S extends StatusCode ? S : never, F extends ResponseFormat ? F : never>
+ :
| ClientResponse<O, S extends StatusCode ? S : never, F extends ResponseFormat ? F : never>
| ClientResponse<
DefaultResponse,
@@ -158,33 +164,27 @@ export type InferRequestOptionsType<T> = T extends (
? NonNullable<R>
: never
-type PathChaining<
- Path extends string,
- E extends Schema,
- DefaultResponse = {}
-> = PathToChain<Path, E, Path, DefaultResponse>
+type PathChaining<Path extends string, E extends Schema> = PathToChain<Path, E, Path>
type PathToChain<
Path extends string,
E extends Schema,
- Original extends string = Path,
- DefaultResponse = {}
+ Original extends string = Path
> = Path extends `/${infer P}`
- ? PathToChain<P, E, Path, DefaultResponse>
+ ? PathToChain<P, E, Path>
: Path extends `${infer P}/${infer R}`
- ? { [K in P]: PathToChain<R, E, Original, DefaultResponse> }
+ ? { [K in P]: PathToChain<R, E, Original> }
: {
[K in Path extends '' ? 'index' : Path]: ClientRequest<
- E extends Record<string, unknown> ? E[Original] : never,
- DefaultResponse
+ E extends Record<string, unknown> ? E[Original] : never
>
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type Client<T, DefaultResponse = {}> = T extends Hono<any, infer S, any>
+export type Client<T> = T extends Hono<any, infer S, any>
? S extends Record<infer K, Schema>
? K extends string
- ? PathChaining<K, S, DefaultResponse>
+ ? PathChaining<K, S>
: never
: never
: never |
This is just my opinion, but server side should accept the default response type instead of client methods like However, I have a feeling that adding this functionality to server side requires much more effort than adding to client 🤔 |
@m-shaka Thank you for your comment! @NamesMT What do you think of this API? #3292 (comment) |
Hi @yusukebe, sorry I was a bit busy preparing for my country's holiday with family All of the implementations make sense to me, though, it would be great if I can declare the default response once on the upper chain and use it for any routes, instead of having to pass it to every API call. I'm thinking of something like this, it would allow configuring type HCDefaultResponse = (
{
code: 'unknownErr'
message: string
detail: any
} |
{
code: 'validationErr'
message: string
detail: string[][]
}
)
const hClient = hc<AppType>('/') // Current normal client
const hClientWithDefault = hClient.$withDR<HCDefaultResponse>() // Client with DefaultResponse applied from root level
const bookApi = hClient.book.$withDR<HCDefaultResponse>() // Client for book APIs with DefaultResponse applied
// Changing the DefaultResponse type as needed
const specialBookRouteRes = await bookApi.specialRoute.$withDR(SpecialDR).$post() What do you think about it? |
Hi @NamesMT It looks good! We should consider the naming if we go with |
Hi @yusukebe |
Okay! Enjoy your time! |
The PR will improve the handling of TypeScript types of status codes in the Response returned by the client.
Fixes #2719
Fixes #3170
Fixes honojs/middleware#580
Allowing
number
forres.status
Imagine the following application which throws the
HTTPException
in the handler:You will create the client with the types:
The current pain point is it can't infer the response having
500
status correctly.The error showing in your IDE:
It should be fixed. However, handling the response content and status with an
HTTPException
orError
object is impossible. So, I've found the solution to enable acceptingnumber
forres.status
.Accept generics for
res.json()
That solution solves the problem of the error of
res.status
, but the content returned fromres.json()
will not be typed:Also to set the correct types is impossible for it. So, I'll introduce
res.json()
accepting generrics. You can write the following:This method does not allow automatic inference, but it avoids type errors.
Other improvements
I added some modifications since other tests failed with the changes.
The author should do the following, if applicable
bun run format:fix && bun run lint:fix
to format the code