From 36707163116874b7a2cd5a337cbee9832158f163 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Sun, 18 Aug 2024 18:22:30 +0900 Subject: [PATCH] feat(client): improve handling status code types --- src/client/types.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++ src/client/types.ts | 24 ++++++++++---- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/client/types.test.ts b/src/client/types.test.ts index 7619dabc..6cbd5d28 100644 --- a/src/client/types.test.ts +++ b/src/client/types.test.ts @@ -1,7 +1,12 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { expectTypeOf } from 'vitest' import { Hono } from '..' import { upgradeWebSocket } from '../adapter/deno/websocket' import { hc } from '.' +import { HTTPException } from '../http-exception' +import type { Equal, Expect } from '../utils/types' +import { setupServer } from 'msw/node' +import { HttpResponse, http } from 'msw' describe('WebSockets', () => { const app = new Hono() @@ -57,3 +62,65 @@ describe('with the leading slash', () => { expectTypeOf(client.foo[':id'].baz).toHaveProperty('$get') }) }) + +describe('Status Code - test only Types', () => { + const server = setupServer( + http.get('http://localhost/foo', async () => { + return HttpResponse.json({}) + }) + ) + + beforeAll(() => server.listen()) + afterEach(() => server.resetHandlers()) + afterAll(() => server.close()) + + const app = new Hono() + const flag = {} + + const routes = app.get('/foo', (c) => { + if (flag) { + throw new HTTPException(500, { + message: 'Server Error!', + }) + } + if (flag) { + return c.json({ message: 'invalid!' }, 401) + } + if (flag) { + return c.redirect('/', 301) + } + return c.json({ ok: true }, 200) + }) + + const client = hc('http://localhost') + + it('Should handle different status codes', async () => { + const res = await client.foo.$get() + + if (res.status === 500) { + const data = await res.json<{ errorMessage: string }>() + type Expected = { errorMessage: string } + type verify = Expect> + } + + if (res.status === 401) { + const data = await res.json() + type Expected = { message: string } + type verify = Expect> + } + + if (res.status === 200) { + const data = await res.json() + type Expected = { ok: boolean } + type verify = Expect> + } + }) + + it('Should infer union types', async () => { + const res = await client.foo.$get() + + const data = await res.json() + type Expected = { message: string } | { ok: boolean } + type verify = Expect> + }) +}) diff --git a/src/client/types.ts b/src/client/types.ts index e64a98eb..33260981 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -1,6 +1,6 @@ import type { Hono } from '../hono' import type { Endpoint, ResponseFormat, Schema } from '../types' -import type { StatusCode, SuccessStatusCode } from '../utils/http-status' +import type { RedirectStatusCode, StatusCode, SuccessStatusCode } from '../utils/http-status' import type { HasRequiredKeys } from '../utils/types' type HonoRequest = (typeof Hono.prototype)['request'] @@ -63,7 +63,15 @@ type ClientResponseOfEndpoint = T extends { outputFormat: infer F status: infer S } - ? ClientResponse + ? F extends 'redirect' + ? ClientResponse + : + | ClientResponse + | ClientResponse< + {}, + S extends StatusCode ? Exclude, S> : never, + F extends ResponseFormat ? F : never + > : never export interface ClientResponse< @@ -84,11 +92,13 @@ export interface ClientResponse< url: string redirect(url: string, status: number): Response clone(): Response - json(): F extends 'text' + json(): F extends 'text' ? Promise : F extends 'json' - ? Promise> - : Promise + ? undefined extends JSONT + ? Promise> + : Promise + : Promise text(): F extends 'text' ? (T extends string ? Promise : Promise) : Promise blob(): Promise formData(): Promise @@ -119,12 +129,14 @@ export type InferResponseType = InferRespo U > +type WithoutEmptyObject = T extends {} ? (keyof T extends never ? never : T) : T + type InferResponseTypeFromEndpoint = T extends { output: infer O status: infer S } ? S extends U - ? O + ? WithoutEmptyObject : never : never