From 9237876b43b26e7bec861ee29fe235b8a1b15ce9 Mon Sep 17 00:00:00 2001 From: m-shaka Date: Mon, 1 Jul 2024 15:57:15 +0900 Subject: [PATCH 1/2] feat: improve JSONParsed --- src/context.ts | 24 +++++-- src/types.test.ts | 4 +- src/utils/types.test.ts | 136 ++++++++++++++++++++++++++++++++++++++++ src/utils/types.ts | 57 +++++++++++++++-- 4 files changed, 209 insertions(+), 12 deletions(-) diff --git a/src/context.ts b/src/context.ts index 2793ae905..31961156c 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 784e24188..640de9198 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 } @@ -528,7 +528,7 @@ describe('Path parameters', () => { type Output = T['/api/:a/:b?']['$get']['output'] type Expected = { a: string - b: string | undefined + b?: string | undefined } type verify = Expect> }) diff --git a/src/utils/types.test.ts b/src/utils/types.test.ts index 4f465149f..5d8f36c64 100644 --- a/src/utils/types.test.ts +++ b/src/utils/types.test.ts @@ -21,6 +21,142 @@ 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 optional', () => { + type Actual = JSONParsed<{ a: number; b: number | undefined }> + type Expected = { a: number; b?: number | undefined } + 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 8754a54a7..dd4f330d3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -24,21 +24,70 @@ 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 + +/** + * @typeParams T - union type + * @example + * IncludeInvalidButNotAll returns `false` + * IncludeInvalidButNotAll returns `true` + * IncludeInvalidButNotAll returns `true` because some branches are invalid + * IncludeInvalidButNotAll returns `false` because all branches are invalid + */ +type IncludeInvalidButNotAll = boolean extends IsInvalid ? true : false + +type Flatten = { [K in keyof T]: T[K] } +/** + * symbol keys are omitted through `JSON.stringify` + */ +type OmitSymbolKeys = { [K in Exclude]: T[K] } +/** + * if the value is an invalid value, its key is omitted + */ +type OmitInvalidValueKeys = { [K in keyof T as T[K] extends InvalidJSONValue ? 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 } + ? Flatten< + // if the value is T | undefined, its key is converted to optional + Partial<{ + [K in keyof OmitSymbolKeys as IncludeInvalidButNotAll extends true + ? K + : never]: JSONParsed + }> & { + [K in keyof OmitInvalidValueKeys> as IncludeInvalidButNotAll< + T[K] + > extends false + ? K + : never]: JSONParsed + } + > : never /** From 4ec9c4a3ed55f68574eadf58d68f1a5b63750683 Mon Sep 17 00:00:00 2001 From: m-shaka Date: Thu, 4 Jul 2024 16:42:27 +0900 Subject: [PATCH 2/2] refactor: make things simple --- src/types.test.ts | 2 +- src/utils/types.test.ts | 9 +++++++-- src/utils/types.ts | 36 ++++++------------------------------ 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/types.test.ts b/src/types.test.ts index 640de9198..b7e87577c 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -528,7 +528,7 @@ describe('Path parameters', () => { type Output = T['/api/:a/:b?']['$get']['output'] type Expected = { a: string - b?: string | undefined + b: string | undefined } type verify = Expect> }) diff --git a/src/utils/types.test.ts b/src/utils/types.test.ts index 5d8f36c64..709ea6896 100644 --- a/src/utils/types.test.ts +++ b/src/utils/types.test.ts @@ -137,9 +137,14 @@ describe('JSONParsed', () => { type Expected = { a: number } expectTypeOf().toEqualTypeOf() }) - it('should convert T | undefined to optional', () => { + it('should convert T | undefined to T | undefined', () => { type Actual = JSONParsed<{ a: number; b: number | undefined }> - type Expected = { 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() }) }) diff --git a/src/utils/types.ts b/src/utils/types.ts index dd4f330d3..8beb970a6 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -33,25 +33,10 @@ type InvalidToNull = T extends InvalidJSONValue ? null : T type IsInvalid = T extends InvalidJSONValue ? true : false -/** - * @typeParams T - union type - * @example - * IncludeInvalidButNotAll returns `false` - * IncludeInvalidButNotAll returns `true` - * IncludeInvalidButNotAll returns `true` because some branches are invalid - * IncludeInvalidButNotAll returns `false` because all branches are invalid - */ -type IncludeInvalidButNotAll = boolean extends IsInvalid ? true : false - -type Flatten = { [K in keyof T]: T[K] } /** * symbol keys are omitted through `JSON.stringify` */ -type OmitSymbolKeys = { [K in Exclude]: T[K] } -/** - * if the value is an invalid value, its key is omitted - */ -type OmitInvalidValueKeys = { [K in keyof T as T[K] extends InvalidJSONValue ? never : K]: T[K] } +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`: @@ -74,20 +59,11 @@ export type JSONParsed = T extends { toJSON(): infer J } : T extends Set | Map ? {} : T extends object - ? Flatten< - // if the value is T | undefined, its key is converted to optional - Partial<{ - [K in keyof OmitSymbolKeys as IncludeInvalidButNotAll extends true - ? K - : never]: JSONParsed - }> & { - [K in keyof OmitInvalidValueKeys> as IncludeInvalidButNotAll< - T[K] - > extends false - ? K - : never]: JSONParsed - } - > + ? { + [K in keyof OmitSymbolKeys as IsInvalid extends true + ? never + : K]: boolean extends IsInvalid ? JSONParsed | undefined : JSONParsed + } : never /**