Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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')
}),
Expand Down Expand Up @@ -151,6 +169,26 @@ describe('Basic - JSON', () => {
expect(data).toStrictEqual({})
})

it('Should get a `{}` content', async () => {
const client = hc<AppType>('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<AppType>('http://localhost')
const res = await client.bigint.$get()
const data = await res.json()
expectTypeOf(data).toMatchTypeOf<never>()
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
Expand Down
13 changes: 2 additions & 11 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SimplifyDeepArray<T> extends JSONValue ? (JSONValue extends SimplifyDeepArray<T> ? never : JSONParsed<T>) : never, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types.
* @returns {Response & TypedResponse<JSONParsed<T>, U, 'json'>} - The response after rendering the JSON object, typed with the provided object and status code types.
*/
type JSONRespondReturn<
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends ContentfulStatusCode
> = Response &
TypedResponse<
SimplifyDeepArray<T> extends JSONValue
? JSONValue extends SimplifyDeepArray<T>
? never
: JSONParsed<T>
: never,
U,
'json'
>
> = Response & TypedResponse<JSONParsed<T>, U, 'json'>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change could be a seperate PR.

From what I understand JSONParsed already contains types that do the same thing as SimplifyDeepArray, specifically

...
  : T extends ReadonlyArray<unknown>
  ? { [K in keyof T]: JSONParsed<InvalidToNull<T[K]>> }
  : ...

compared with SimplifyDeepArray

type SimplifyDeepArray<T> = T extends any[]
  ? { [E in keyof T]: SimplifyDeepArray<T[E]> }
  : Simplify<T>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. We can remove SimplifyDeepArray. For now, I'll merge this PR.


/**
* Interface representing a function that responds with HTML content.
Expand Down
74 changes: 68 additions & 6 deletions src/utils/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Equal, Expect, JSONParsed } from './types'
import type { JSONParsed, JSONValue } from './types'

describe('JSONParsed', () => {
enum SampleEnum {
Expand Down Expand Up @@ -78,6 +78,11 @@ describe('JSONParsed', () => {
type Expected = never
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert bigint type to never', () => {
type Actual = JSONParsed<bigint>
type Expected = never
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('array', () => {
Expand Down Expand Up @@ -154,6 +159,20 @@ describe('JSONParsed', () => {
})
})

describe('unknown', () => {
it('should convert unknown type to unknown', () => {
type Actual = JSONParsed<unknown>
type Expected = JSONValue
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should convert unknown value to JSONValue', () => {
type Actual = JSONParsed<{ value: unknown }>
type Expected = { value: JSONValue }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('Set/Map', () => {
it('should convert Set to empty object', () => {
type Actual = JSONParsed<Set<number>>
Expand Down Expand Up @@ -199,19 +218,62 @@ describe('JSONParsed', () => {
datetime: string
}
type Actual = JSONParsed<Post>
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type verify = Expect<Equal<Expected, Actual>>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should convert bigint to never', () => {
type Post = {
num: bigint
}
type Expected = never
type Actual = JSONParsed<Post>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should convert bigint when TError is provided', () => {
type Post = {
num: bigint
}
type Expected = {
num: never
num: JSONValue
}
type Actual = JSONParsed<Post, never>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should convert bigint[] to never', () => {
type Post = {
nums: bigint[]
}
type Expected = never
type Actual = JSONParsed<Post>
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type verify = Expect<Equal<Expected, Actual>>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should convert bigint[] when TError is provided', () => {
type Post = {
num: bigint[]
}
type Expected = {
num: JSONValue[]
}
type Actual = JSONParsed<Post, never>
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})

it('Should parse bigint with a toJSON function', () => {
class SafeBigInt {
unsafe = BigInt('42')

toJSON() {
return {
unsafe: '42n',
}
}
}

type Actual = JSONParsed<SafeBigInt>
type Expected = { unsafe: string }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})
42 changes: 32 additions & 10 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,49 @@ type IsInvalid<T> = T extends InvalidJSONValue ? true : false
type OmitSymbolKeys<T> = { [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> = 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<bigint>`.
* You can set it to `never` to disable this check.
*/
export type JSONParsed<T, TError = bigint | ReadonlyArray<bigint>> = T extends TError
? never
: T extends {
toJSON(): infer J
}
? (() => J) extends () => JSONPrimitive
? J
: (() => J) extends () => { toJSON(): unknown }
? {}
: JSONParsed<J>
: JSONParsed<J, TError>
: T extends JSONPrimitive
? T
: T extends InvalidJSONValue
? never
: T extends ReadonlyArray<unknown>
? { [K in keyof T]: JSONParsed<InvalidToNull<T[K]>> }
: T extends Set<unknown> | Map<unknown, unknown>
? { [K in keyof T]: JSONParsed<InvalidToNull<T[K]>, TError> }
: T extends Set<unknown> | Map<unknown, unknown> | Record<string, never>
? {}
: T extends object
? {
[K in keyof OmitSymbolKeys<T> as IsInvalid<T[K]> extends true
? never
: K]: boolean extends IsInvalid<T[K]> ? JSONParsed<T[K]> | undefined : JSONParsed<T[K]>
}
? T[keyof T] extends TError
? never
: {
[K in keyof OmitSymbolKeys<T> as IsInvalid<T[K]> extends true
? never
: K]: boolean extends IsInvalid<T[K]>
? JSONParsed<T[K], TError> | undefined
: JSONParsed<T[K], TError>
}
: T extends unknown
? JSONValue
: never

/**
Expand Down
Loading