diff --git a/src/client/client.test.ts b/src/client/client.test.ts index 8e8144c01..5779c0b31 100644 --- a/src/client/client.test.ts +++ b/src/client/client.test.ts @@ -12,6 +12,16 @@ import { validator } from '../validator' import { hc } from './client' import type { ClientResponse, InferRequestType, InferResponseType } from './types' +class SafeBigInt { + unsafe = BigInt(42) + + toJSON() { + return { + value: '42n', + } + } +} + describe('Basic - JSON', () => { const app = new Hono() @@ -51,6 +61,8 @@ describe('Basic - JSON', () => { .get('/hello-not-found', (c) => c.notFound()) .get('/null', (c) => c.json(null)) .get('/empty', (c) => c.json({})) + .get('/bigint', (c) => c.json({ value: BigInt(42) })) + .get('/safe-bigint', (c) => c.json(new SafeBigInt())) type AppType = typeof route @@ -81,6 +93,12 @@ describe('Basic - JSON', () => { http.get('http://localhost/empty', () => { return HttpResponse.json({}) }), + http.get('http://localhost/bigint', () => { + return HttpResponse.json({ value: BigInt(42) }) + }), + http.get('http://localhost/safe-bigint', () => { + return HttpResponse.json(new SafeBigInt()) + }), http.get('http://localhost/api/string', () => { return HttpResponse.json('a-string') }), @@ -151,6 +169,26 @@ describe('Basic - JSON', () => { expect(data).toStrictEqual({}) }) + it('Should get a `{}` content', async () => { + const client = hc('http://localhost') + const res = await client['safe-bigint'].$get() + const data = await res.json() + expectTypeOf(data).toMatchTypeOf<{ value: string }>() + expect(data).toStrictEqual({ value: '42n' }) + }) + + it('Should get an error response', async () => { + const client = hc('http://localhost') + const res = await client.bigint.$get() + const data = await res.json() + expectTypeOf(data).toMatchTypeOf() + expect(res.status).toBe(500) + expect(data).toMatchObject({ + message: 'Do not know how to serialize a BigInt', + name: 'TypeError', + }) + }) + it('Should have correct types - primitives', async () => { const app = new Hono() const route = app diff --git a/src/context.ts b/src/context.ts index 408515813..a63b8b6dc 100644 --- a/src/context.ts +++ b/src/context.ts @@ -194,21 +194,12 @@ interface JSONRespond { * @template T - The type of the JSON value or simplified unknown type. * @template U - The type of the status code. * - * @returns {Response & TypedResponse extends JSONValue ? (JSONValue extends SimplifyDeepArray ? never : JSONParsed) : never, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types. + * @returns {Response & TypedResponse, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types. */ type JSONRespondReturn< T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, U extends ContentfulStatusCode -> = Response & - TypedResponse< - SimplifyDeepArray extends JSONValue - ? JSONValue extends SimplifyDeepArray - ? never - : JSONParsed - : never, - U, - 'json' - > +> = Response & TypedResponse, U, 'json'> /** * Interface representing a function that responds with HTML content. diff --git a/src/utils/types.test.ts b/src/utils/types.test.ts index 80fca294d..cc703a9e0 100644 --- a/src/utils/types.test.ts +++ b/src/utils/types.test.ts @@ -1,4 +1,4 @@ -import type { Equal, Expect, JSONParsed } from './types' +import type { JSONParsed, JSONValue } from './types' describe('JSONParsed', () => { enum SampleEnum { @@ -78,6 +78,11 @@ describe('JSONParsed', () => { type Expected = never expectTypeOf().toEqualTypeOf() }) + it('should convert bigint type to never', () => { + type Actual = JSONParsed + type Expected = never + expectTypeOf().toEqualTypeOf() + }) }) describe('array', () => { @@ -154,6 +159,20 @@ describe('JSONParsed', () => { }) }) + describe('unknown', () => { + it('should convert unknown type to unknown', () => { + type Actual = JSONParsed + type Expected = JSONValue + expectTypeOf().toEqualTypeOf() + }) + + it('Should convert unknown value to JSONValue', () => { + type Actual = JSONParsed<{ value: unknown }> + type Expected = { value: JSONValue } + expectTypeOf().toEqualTypeOf() + }) + }) + describe('Set/Map', () => { it('should convert Set to empty object', () => { type Actual = JSONParsed> @@ -199,19 +218,62 @@ describe('JSONParsed', () => { datetime: string } type Actual = JSONParsed - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type verify = Expect> + expectTypeOf().toEqualTypeOf() }) it('Should convert bigint to never', () => { + type Post = { + num: bigint + } + type Expected = never + type Actual = JSONParsed + expectTypeOf().toEqualTypeOf() + }) + + it('Should convert bigint when TError is provided', () => { type Post = { num: bigint } type Expected = { - num: never + num: JSONValue } + type Actual = JSONParsed + expectTypeOf().toEqualTypeOf() + }) + + it('Should convert bigint[] to never', () => { + type Post = { + nums: bigint[] + } + type Expected = never type Actual = JSONParsed - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type verify = Expect> + expectTypeOf().toEqualTypeOf() + }) + + it('Should convert bigint[] when TError is provided', () => { + type Post = { + num: bigint[] + } + type Expected = { + num: JSONValue[] + } + type Actual = JSONParsed + expectTypeOf().toEqualTypeOf() + }) + + it('Should parse bigint with a toJSON function', () => { + class SafeBigInt { + unsafe = BigInt('42') + + toJSON() { + return { + unsafe: '42n', + } + } + } + + type Actual = JSONParsed + type Expected = { unsafe: string } + expectTypeOf().toEqualTypeOf() }) }) diff --git a/src/utils/types.ts b/src/utils/types.ts index 0c1247851..b1552a1b9 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -41,27 +41,49 @@ type IsInvalid = T extends InvalidJSONValue ? true : false type OmitSymbolKeys = { [K in keyof T as K extends symbol ? never : K]: T[K] } export type JSONValue = JSONObject | JSONArray | JSONPrimitive -// Non-JSON values such as `Date` implement `.toJSON()`, so they can be transformed to a value assignable to `JSONObject`: -export type JSONParsed = T extends { toJSON(): infer J } +/** + * Convert a type to a JSON-compatible type. + * + * Non-JSON values such as `Date` implement `.toJSON()`, + * so they can be transformed to a value assignable to `JSONObject` + * + * `JSON.stringify()` throws a `TypeError` when it encounters a `bigint` value, + * unless a custom `replacer` function or `.toJSON()` method is provided. + * + * This behaviour can be controlled by the `TError` generic type parameter, + * which defaults to `bigint | ReadonlyArray`. + * You can set it to `never` to disable this check. + */ +export type JSONParsed> = T extends TError + ? never + : T extends { + toJSON(): infer J + } ? (() => J) extends () => JSONPrimitive ? J : (() => J) extends () => { toJSON(): unknown } ? {} - : JSONParsed + : JSONParsed : T extends JSONPrimitive ? T : T extends InvalidJSONValue ? never : T extends ReadonlyArray - ? { [K in keyof T]: JSONParsed> } - : T extends Set | Map + ? { [K in keyof T]: JSONParsed, TError> } + : T extends Set | Map | Record ? {} : T extends object - ? { - [K in keyof OmitSymbolKeys as IsInvalid extends true - ? never - : K]: boolean extends IsInvalid ? JSONParsed | undefined : JSONParsed - } + ? T[keyof T] extends TError + ? never + : { + [K in keyof OmitSymbolKeys as IsInvalid extends true + ? never + : K]: boolean extends IsInvalid + ? JSONParsed | undefined + : JSONParsed + } + : T extends unknown + ? JSONValue : never /**