0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-24 19:26:56 +01:00

feat(jsonT): check JSON type (#939)

* feat(`jsonT`): check JSON type

* denoify

* use `JSONValue`

* create `TrueAndFalseToBoolean` and add a comment
This commit is contained in:
Yusuke Wada 2023-03-02 08:56:13 +09:00 committed by GitHub
parent 73860a1595
commit b1c5059708
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 8 deletions

View File

@ -4,6 +4,7 @@ import type { Env, NotFoundHandler, Input } from './types.ts'
import type { CookieOptions } from './utils/cookie.ts'
import { serialize } from './utils/cookie.ts'
import type { StatusCode } from './utils/http-status.ts'
import type { PrettyJSON, JSONValue } from './utils/types.ts'
type Runtime = 'node' | 'deno' | 'bun' | 'workerd' | 'fastly' | 'edge-light' | 'lagon' | 'other'
type HeaderRecord = Record<string, string | string[]>
@ -237,14 +238,15 @@ export class Context<
return this.newResponse(body, status, headers)
}
jsonT = <T = object>(
object: T,
jsonT = <T>(
object: T extends JSONValue ? T : JSONValue,
status: StatusCode = this._status,
headers?: HeaderRecord
): TypedResponse<T> => {
): TypedResponse<T extends JSONValue ? (JSONValue extends T ? never : PrettyJSON<T>) : never> => {
return {
response: this.json(object, status, headers),
data: object,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: object as any,
format: 'json',
}
}

View File

@ -10,3 +10,21 @@ export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) ex
) => void
? I
: never
export type JSONPrimitive = string | boolean | number | null | undefined
export type JSONArray = (JSONPrimitive | JSONObject | JSONArray)[]
export type JSONObject = { [key: string]: JSONPrimitive | JSONArray | JSONObject }
export type JSONValue = JSONObject | JSONArray | JSONPrimitive
// `boolean` will be `true` or `false` because it's an alias of them.
// See: https://github.com/microsoft/TypeScript/issues/22596
// This type converts `true | false` to `boolean` that we expect.
type TrueAndFalseToBoolean<T> = T extends true ? boolean : T extends false ? boolean : T
export type PrettyJSON<T> = T extends JSONPrimitive
? TrueAndFalseToBoolean<T>
: T extends JSONArray
? PrettyJSON<T[number]>
: T extends JSONObject
? { [K in keyof T]: PrettyJSON<T[K]> }
: never

View File

@ -332,6 +332,17 @@ describe('Merge path with `app.route()`', () => {
expect(data.ok).toBe(true)
})
it('Should not allow the incorrect JSON type', async () => {
const app = new Hono()
// @ts-ignore
const route = app.get('/api/foo', (c) => c.jsonT({ datetime: new Date() }))
type AppType = typeof route
const client = hc<AppType>('http://localhost')
const res = await client.api.foo.$get()
const data = await res.json()
type verify = Expect<Equal<never, typeof data>>
})
describe('Multiple endpoints', () => {
const api = new Hono()
.get('/foo', (c) => c.jsonT({ foo: '' }))

View File

@ -4,6 +4,7 @@ import type { Env, NotFoundHandler, Input } from './types'
import type { CookieOptions } from './utils/cookie'
import { serialize } from './utils/cookie'
import type { StatusCode } from './utils/http-status'
import type { PrettyJSON, JSONValue } from './utils/types'
type Runtime = 'node' | 'deno' | 'bun' | 'workerd' | 'fastly' | 'edge-light' | 'lagon' | 'other'
type HeaderRecord = Record<string, string | string[]>
@ -237,14 +238,15 @@ export class Context<
return this.newResponse(body, status, headers)
}
jsonT = <T = object>(
object: T,
jsonT = <T>(
object: T extends JSONValue ? T : JSONValue,
status: StatusCode = this._status,
headers?: HeaderRecord
): TypedResponse<T> => {
): TypedResponse<T extends JSONValue ? (JSONValue extends T ? never : PrettyJSON<T>) : never> => {
return {
response: this.json(object, status, headers),
data: object,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: object as any,
format: 'json',
}
}

View File

@ -10,3 +10,21 @@ export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) ex
) => void
? I
: never
export type JSONPrimitive = string | boolean | number | null | undefined
export type JSONArray = (JSONPrimitive | JSONObject | JSONArray)[]
export type JSONObject = { [key: string]: JSONPrimitive | JSONArray | JSONObject }
export type JSONValue = JSONObject | JSONArray | JSONPrimitive
// `boolean` will be `true` or `false` because it's an alias of them.
// See: https://github.com/microsoft/TypeScript/issues/22596
// This type converts `true | false` to `boolean` that we expect.
type TrueAndFalseToBoolean<T> = T extends true ? boolean : T extends false ? boolean : T
export type PrettyJSON<T> = T extends JSONPrimitive
? TrueAndFalseToBoolean<T>
: T extends JSONArray
? PrettyJSON<T[number]>
: T extends JSONObject
? { [K in keyof T]: PrettyJSON<T[K]> }
: never