Skip to content

Commit

Permalink
feat(types): improve JSONParsed (#3074)
Browse files Browse the repository at this point in the history
* feat: improve JSONParsed

* refactor: make things simple
  • Loading branch information
m-shaka authored Jul 6, 2024
1 parent 6c1b2a7 commit 2a4f257
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 11 deletions.
24 changes: 18 additions & 6 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import type {
} from './types'
import { HtmlEscapedCallbackPhase, resolveCallback } from './utils/html'
import type { RedirectStatusCode, StatusCode } from './utils/http-status'
import type { IsAny, JSONParsed, JSONValue, SimplifyDeepArray } from './utils/types'
import type {
InvalidJSONValue,
IsAny,
JSONParsed,
JSONValue,
SimplifyDeepArray,
} from './utils/types'

type HeaderRecord = Record<string, string | string[]>

Expand Down Expand Up @@ -142,12 +148,18 @@ interface TextRespond {
* @returns {JSONRespondReturn<T, U>} - The response after rendering the JSON object, typed with the provided object and status code types.
*/
interface JSONRespond {
<T extends JSONValue | SimplifyDeepArray<unknown>, U extends StatusCode = StatusCode>(
<
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode = StatusCode
>(
object: T,
status?: U,
headers?: HeaderRecord
): JSONRespondReturn<T, U>
<T extends JSONValue | SimplifyDeepArray<unknown>, U extends StatusCode = StatusCode>(
<
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode = StatusCode
>(
object: T,
init?: ResponseInit
): JSONRespondReturn<T, U>
Expand All @@ -160,15 +172,15 @@ interface JSONRespond {
* @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.
*/
type JSONRespondReturn<
T extends JSONValue | SimplifyDeepArray<unknown>,
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode
> = Response &
TypedResponse<
SimplifyDeepArray<T> extends JSONValue
? JSONValue extends SimplifyDeepArray<T>
? never
: JSONParsed<T>
: never,
: JSONParsed<T>,
U,
'json'
>
Expand Down Expand Up @@ -692,7 +704,7 @@ export class Context<
* ```
*/
json: JSONRespond = <
T extends JSONValue | SimplifyDeepArray<unknown>,
T extends JSONValue | SimplifyDeepArray<unknown> | InvalidJSONValue,
U extends StatusCode = StatusCode
>(
object: T,
Expand Down
2 changes: 1 addition & 1 deletion src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ describe('Support c.json(undefined)', () => {
'/this/is/a/test': {
$get: {
input: {}
output: undefined
output: never
outputFormat: 'json'
status: StatusCode
}
Expand Down
141 changes: 141 additions & 0 deletions src/utils/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,147 @@ describe('JSONParsed', () => {
someMeta: Meta
}

describe('primitives', () => {
it('should convert number type to number', () => {
type Actual = JSONParsed<number>
type Expected = number
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert string type to string', () => {
type Actual = JSONParsed<string>
type Expected = string
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert boolean type to boolean', () => {
type Actual = JSONParsed<boolean>
type Expected = boolean
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert null type to null', () => {
type Actual = JSONParsed<null>
type Expected = null
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('toJSON', () => {
it('should convert { toJSON() => T } to T', () => {
type Actual = JSONParsed<{ toJSON(): number }>
type Expected = number
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('toJSON is not called recursively', () => {
type Actual = JSONParsed<{ toJSON(): { toJSON(): number } }>
type Expected = {}
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert { a: { toJSON() => T } } to { a: T }', () => {
type Actual = JSONParsed<{ a: { toJSON(): number } }>
type Expected = { a: number }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('invalid types', () => {
it('should convert undefined type to never', () => {
type Actual = JSONParsed<undefined>
type Expected = never
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert symbol type to never', () => {
type Actual = JSONParsed<symbol>
type Expected = never
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert function type to never', () => {
type Actual = JSONParsed<() => void>
type Expected = never
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('array', () => {
it('should convert undefined[] type to null[]', () => {
type Actual = JSONParsed<undefined[]>
type Expected = null[]
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert Function[] type to null[]', () => {
type Actual = JSONParsed<(() => void)[]>
type Expected = null[]
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert symbol[] type to null[]', () => {
type Actual = JSONParsed<symbol[]>
type Expected = null[]
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert (T | undefined)[] type to JSONParsedT | null>[]', () => {
type Actual = JSONParsed<(number | undefined)[]>
type Expected = (number | null)[]
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('tuple', () => {
it('should convert [T, S] type to [T, S]', () => {
type Actual = JSONParsed<[number, string]>
type Expected = [number, string]
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert [T, undefined] type to [T, null]', () => {
type Actual = JSONParsed<[number, undefined]>
type Expected = [number, null]
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('object', () => {
it('should omit keys with undefined value', () => {
type Actual = JSONParsed<{ a: number; b: undefined }>
type Expected = { a: number }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should omit keys with symbol value', () => {
type Actual = JSONParsed<{ a: number; b: symbol }>
type Expected = { a: number }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should omit keys with function value', () => {
type Actual = JSONParsed<{ a: number; b: () => void }>
type Expected = { a: number }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should omit symbol keys', () => {
type Actual = JSONParsed<{ a: number; [x: symbol]: number }>
type Expected = { a: number }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert T | undefined to T | undefined', () => {
type Actual = JSONParsed<{ a: number; b: number | undefined }>
type Expected = { a: number; b: number | undefined }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should omit keys with invalid union', () => {
type Actual = JSONParsed<{ a: number; b: undefined | symbol }>
type Expected = { a: number }
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

describe('Set/Map', () => {
it('should convert Set to empty object', () => {
type Actual = JSONParsed<Set<number>>
type Expected = {}
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
it('should convert Map to empty object', () => {
type Actual = JSONParsed<Map<number, number>>
type Expected = {}
expectTypeOf<Actual>().toEqualTypeOf<Expected>()
})
})

it('Should parse a complex type', () => {
const sample: JSONParsed<SampleType> = {
someMeta: {
Expand Down
33 changes: 29 additions & 4 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,46 @@ export type RemoveBlankRecord<T> = T extends Record<infer K, unknown>

export type IfAnyThenEmptyObject<T> = 0 extends 1 & T ? {} : T

export type JSONPrimitive = string | boolean | number | null | undefined
export type JSONPrimitive = string | boolean | number | null
export type JSONArray = (JSONPrimitive | JSONObject | JSONArray)[]
export type JSONObject = { [key: string]: JSONPrimitive | JSONArray | JSONObject | object }
export type InvalidJSONValue = undefined | symbol | ((...args: unknown[]) => unknown)

type InvalidToNull<T> = T extends InvalidJSONValue ? null : T

type IsInvalid<T> = T extends InvalidJSONValue ? true : false

/**
* symbol keys are omitted through `JSON.stringify`
*/
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 }
? (() => J) extends () => JSONObject
? (() => J) extends () => JSONPrimitive
? J
: (() => J) extends () => { toJSON(): unknown }
? {}
: JSONParsed<J>
: T extends JSONPrimitive
? T
: T extends InvalidJSONValue
? never
: T extends []
? []
: T extends readonly [infer R, ...infer U]
? [JSONParsed<InvalidToNull<R>>, ...JSONParsed<U>]
: T extends Array<infer U>
? Array<JSONParsed<U>>
? Array<JSONParsed<InvalidToNull<U>>>
: T extends Set<unknown> | Map<unknown, unknown>
? {}
: T extends object
? { [K in keyof T]: JSONParsed<T[K]> }
? {
[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]>
}
: never

/**
Expand Down

0 comments on commit 2a4f257

Please sign in to comment.