From 2a4f257f36a3553e2891074e56af9275ac1f5e3b Mon Sep 17 00:00:00 2001 From: m-shaka Date: Sat, 6 Jul 2024 15:42:33 +0900 Subject: [PATCH] feat(types): improve JSONParsed (#3074) * feat: improve JSONParsed * refactor: make things simple --- src/context.ts | 24 +++++-- src/types.test.ts | 2 +- src/utils/types.test.ts | 141 ++++++++++++++++++++++++++++++++++++++++ src/utils/types.ts | 33 ++++++++-- 4 files changed, 189 insertions(+), 11 deletions(-) diff --git a/src/context.ts b/src/context.ts index acd18389..a24fa26d 100644 --- a/src/context.ts +++ b/src/context.ts @@ -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 @@ -142,12 +148,18 @@ interface TextRespond { * @returns {JSONRespondReturn} - The response after rendering the JSON object, typed with the provided object and status code types. */ interface JSONRespond { - , U extends StatusCode = StatusCode>( + < + T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, + U extends StatusCode = StatusCode + >( object: T, status?: U, headers?: HeaderRecord ): JSONRespondReturn - , U extends StatusCode = StatusCode>( + < + T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, + U extends StatusCode = StatusCode + >( object: T, init?: ResponseInit ): JSONRespondReturn @@ -160,7 +172,7 @@ interface JSONRespond { * @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. */ type JSONRespondReturn< - T extends JSONValue | SimplifyDeepArray, + T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, U extends StatusCode > = Response & TypedResponse< @@ -168,7 +180,7 @@ type JSONRespondReturn< ? JSONValue extends SimplifyDeepArray ? never : JSONParsed - : never, + : JSONParsed, U, 'json' > @@ -692,7 +704,7 @@ export class Context< * ``` */ json: JSONRespond = < - T extends JSONValue | SimplifyDeepArray, + T extends JSONValue | SimplifyDeepArray | InvalidJSONValue, U extends StatusCode = StatusCode >( object: T, diff --git a/src/types.test.ts b/src/types.test.ts index 5ee4d972..eafd2683 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -375,7 +375,7 @@ describe('Support c.json(undefined)', () => { '/this/is/a/test': { $get: { input: {} - output: undefined + output: never outputFormat: 'json' status: StatusCode } diff --git a/src/utils/types.test.ts b/src/utils/types.test.ts index 4f465149..709ea689 100644 --- a/src/utils/types.test.ts +++ b/src/utils/types.test.ts @@ -21,6 +21,147 @@ describe('JSONParsed', () => { someMeta: Meta } + describe('primitives', () => { + it('should convert number type to number', () => { + type Actual = JSONParsed + type Expected = number + expectTypeOf().toEqualTypeOf() + }) + it('should convert string type to string', () => { + type Actual = JSONParsed + type Expected = string + expectTypeOf().toEqualTypeOf() + }) + it('should convert boolean type to boolean', () => { + type Actual = JSONParsed + type Expected = boolean + expectTypeOf().toEqualTypeOf() + }) + it('should convert null type to null', () => { + type Actual = JSONParsed + type Expected = null + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('toJSON', () => { + it('should convert { toJSON() => T } to T', () => { + type Actual = JSONParsed<{ toJSON(): number }> + type Expected = number + expectTypeOf().toEqualTypeOf() + }) + it('toJSON is not called recursively', () => { + type Actual = JSONParsed<{ toJSON(): { toJSON(): number } }> + type Expected = {} + expectTypeOf().toEqualTypeOf() + }) + it('should convert { a: { toJSON() => T } } to { a: T }', () => { + type Actual = JSONParsed<{ a: { toJSON(): number } }> + type Expected = { a: number } + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('invalid types', () => { + it('should convert undefined type to never', () => { + type Actual = JSONParsed + type Expected = never + expectTypeOf().toEqualTypeOf() + }) + it('should convert symbol type to never', () => { + type Actual = JSONParsed + type Expected = never + expectTypeOf().toEqualTypeOf() + }) + it('should convert function type to never', () => { + type Actual = JSONParsed<() => void> + type Expected = never + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('array', () => { + it('should convert undefined[] type to null[]', () => { + type Actual = JSONParsed + type Expected = null[] + expectTypeOf().toEqualTypeOf() + }) + it('should convert Function[] type to null[]', () => { + type Actual = JSONParsed<(() => void)[]> + type Expected = null[] + expectTypeOf().toEqualTypeOf() + }) + it('should convert symbol[] type to null[]', () => { + type Actual = JSONParsed + type Expected = null[] + expectTypeOf().toEqualTypeOf() + }) + it('should convert (T | undefined)[] type to JSONParsedT | null>[]', () => { + type Actual = JSONParsed<(number | undefined)[]> + type Expected = (number | null)[] + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('tuple', () => { + it('should convert [T, S] type to [T, S]', () => { + type Actual = JSONParsed<[number, string]> + type Expected = [number, string] + expectTypeOf().toEqualTypeOf() + }) + it('should convert [T, undefined] type to [T, null]', () => { + type Actual = JSONParsed<[number, undefined]> + type Expected = [number, null] + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('object', () => { + it('should omit keys with undefined value', () => { + type Actual = JSONParsed<{ a: number; b: undefined }> + type Expected = { a: number } + expectTypeOf().toEqualTypeOf() + }) + it('should omit keys with symbol value', () => { + type Actual = JSONParsed<{ a: number; b: symbol }> + type Expected = { a: number } + expectTypeOf().toEqualTypeOf() + }) + it('should omit keys with function value', () => { + type Actual = JSONParsed<{ a: number; b: () => void }> + type Expected = { a: number } + expectTypeOf().toEqualTypeOf() + }) + it('should omit symbol keys', () => { + type Actual = JSONParsed<{ a: number; [x: symbol]: number }> + type Expected = { a: number } + expectTypeOf().toEqualTypeOf() + }) + it('should convert T | undefined to T | undefined', () => { + type Actual = JSONParsed<{ a: number; b: number | undefined }> + type Expected = { a: number; b: number | undefined } + expectTypeOf().toEqualTypeOf() + }) + it('should omit keys with invalid union', () => { + type Actual = JSONParsed<{ a: number; b: undefined | symbol }> + type Expected = { a: number } + expectTypeOf().toEqualTypeOf() + }) + }) + + describe('Set/Map', () => { + it('should convert Set to empty object', () => { + type Actual = JSONParsed> + type Expected = {} + expectTypeOf().toEqualTypeOf() + }) + it('should convert Map to empty object', () => { + type Actual = JSONParsed> + type Expected = {} + expectTypeOf().toEqualTypeOf() + }) + }) + it('Should parse a complex type', () => { const sample: JSONParsed = { someMeta: { diff --git a/src/utils/types.ts b/src/utils/types.ts index 8754a54a..8beb970a 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -24,21 +24,46 @@ export type RemoveBlankRecord = T extends Record export type IfAnyThenEmptyObject = 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 extends InvalidJSONValue ? null : T + +type IsInvalid = T extends InvalidJSONValue ? true : false + +/** + * symbol keys are omitted through `JSON.stringify` + */ +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 } - ? (() => J) extends () => JSONObject + ? (() => J) extends () => JSONPrimitive ? J + : (() => J) extends () => { toJSON(): unknown } + ? {} : JSONParsed : T extends JSONPrimitive ? T + : T extends InvalidJSONValue + ? never + : T extends [] + ? [] + : T extends readonly [infer R, ...infer U] + ? [JSONParsed>, ...JSONParsed] : T extends Array - ? Array> + ? Array>> + : T extends Set | Map + ? {} : T extends object - ? { [K in keyof T]: JSONParsed } + ? { + [K in keyof OmitSymbolKeys as IsInvalid extends true + ? never + : K]: boolean extends IsInvalid ? JSONParsed | undefined : JSONParsed + } : never /**