From 2071f47efbe79cc932ab45137e931a2998901a4e Mon Sep 17 00:00:00 2001 From: Sachin Raja Date: Mon, 28 Nov 2022 01:00:03 -0800 Subject: [PATCH] `Jsonify`: Add jump to definition and `any` support (#519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michaƫl De Boey Co-authored-by: Sindre Sorhus --- source/internal.d.ts | 76 +++++++++++++++++++++++++++++ source/jsonify.d.ts | 95 +++++++++++++++++++------------------ source/set-return-type.d.ts | 3 +- test-d/jsonify.ts | 10 ++++ 4 files changed, 136 insertions(+), 48 deletions(-) diff --git a/source/internal.d.ts b/source/internal.d.ts index ca9ddae72..018ed58b1 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -1,4 +1,5 @@ import type {Primitive} from './primitive'; +import type {Simplify} from './simplify'; /** Returns a boolean for whether the two given types are equal. @@ -108,3 +109,78 @@ export type IsUpperCase = T extends Uppercase ? true : fals Returns a boolean for whether the string is numeric. */ export type IsNumeric = T extends `${number}` ? true : false; + +/** +Returns a boolean for whether the the type is `any`. + +@link https://stackoverflow.com/a/49928360/1490091 +*/ +export type IsAny = 0 extends 1 & T ? true : false; + +/** +For an object T, if it has any properties that are a union with `undefined`, make those into optional properties instead. + +@example +``` +type User = { + firstName: string; + lastName: string | undefined; +}; + +type OptionalizedUser = UndefinedToOptional; +//=> { +// firstName: string; +// lastName?: string; +// } +``` +*/ +export type UndefinedToOptional = Simplify< +{ + // Property is not a union with `undefined`, keep it as-is. + [Key in keyof Pick>]: T[Key]; +} & { + // Property _is_ a union with defined value. Set as optional (via `?`) and remove `undefined` from the union. + [Key in keyof Pick>]?: Exclude; +} +>; + +// Returns `never` if the key or property is not jsonable without testing whether the property is required or optional otherwise return the key. +type BaseKeyFilter = Key extends symbol + ? never + : Type[Key] extends symbol + ? never + : [(...args: any[]) => any] extends [Type[Key]] + ? never + : Key; + +/** +Returns the required keys. +*/ +type FilterDefinedKeys = Exclude< +{ + [Key in keyof T]: IsAny extends true + ? Key + : undefined extends T[Key] + ? never + : T[Key] extends undefined + ? never + : BaseKeyFilter; +}[keyof T], +undefined +>; + +/** +Returns the optional keys. +*/ +type FilterOptionalKeys = Exclude< +{ + [Key in keyof T]: IsAny extends true + ? never + : undefined extends T[Key] + ? T[Key] extends undefined + ? never + : BaseKeyFilter + : never; +}[keyof T], +undefined +>; diff --git a/source/jsonify.d.ts b/source/jsonify.d.ts index d9fc87708..a88d047ad 100644 --- a/source/jsonify.d.ts +++ b/source/jsonify.d.ts @@ -1,32 +1,26 @@ import type {JsonPrimitive, JsonValue} from './basic'; import type {EmptyObject} from './empty-object'; -import type {Merge} from './merge'; +import type {IsAny, UndefinedToOptional} from './internal'; import type {NegativeInfinity, PositiveInfinity} from './numeric'; import type {TypedArray} from './typed-array'; // Note: The return value has to be `any` and not `unknown` so it can match `void`. type NotJsonable = ((...args: any[]) => any) | undefined | symbol; -// Returns never if the key or property is not jsonable without testing whether the property is required or optional otherwise return the key. -type BaseKeyFilter = Key extends symbol - ? never - : Type[Key] extends symbol - ? never - : [(...args: any[]) => any] extends [Type[Key]] - ? never - : Key; - -// Returns never if the key or property is not jsonable or optional otherwise return the key. -type RequiredKeyFilter = undefined extends Type[Key] - ? never - : BaseKeyFilter; - -// Returns never if the key or property is not jsonable or required otherwise return the key. -type OptionalKeyFilter = undefined extends Type[Key] - ? Type[Key] extends undefined - ? never - : BaseKeyFilter - : never; +type JsonifyTuple = { + [Key in keyof T]: T[Key] extends NotJsonable ? null : Jsonify; +}; + +type FilterJsonableKeys = { + [Key in keyof T]: T[Key] extends NotJsonable ? never : Key; +}[keyof T]; + +/** +JSON serialize objects (not including arrays) and classes. +*/ +type JsonifyObject = { + [Key in keyof Pick>]: Jsonify; +}; /** Transform a type to one that is assignable to the `JsonValue` type. @@ -85,29 +79,36 @@ const timeJson = JSON.parse(JSON.stringify(time)) as Jsonify; @category JSON */ -export type Jsonify = - // Check if there are any non-JSONable types represented in the union. - // Note: The use of tuples in this first condition side-steps distributive conditional types - // (see https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532) - [Extract] extends [never] - ? T extends PositiveInfinity | NegativeInfinity ? null - : T extends JsonPrimitive ? T // Primitive is acceptable - : T extends object - // Any object with toJSON is special case - ? T extends {toJSON(): infer J} ? (() => J) extends (() => JsonValue) // Is J assignable to JsonValue? - ? J // Then T is Jsonable and its Jsonable value is J - : Jsonify // Maybe if we look a level deeper we'll find a JsonValue - // Instanced primitives are objects - : T extends Number ? number - : T extends String ? string - : T extends Boolean ? boolean - : T extends Map | Set ? EmptyObject - : T extends TypedArray ? Record - : T extends any[] - ? {[I in keyof T]: T[I] extends NotJsonable ? null : Jsonify} - : Merge< - {[Key in keyof T as RequiredKeyFilter]: Jsonify}, - {[Key in keyof T as OptionalKeyFilter]?: Jsonify>} - > // Recursive call for its children - : never // Otherwise any other non-object is removed - : never; // Otherwise non-JSONable type union was found not empty +export type Jsonify = IsAny extends true + ? any + : T extends PositiveInfinity | NegativeInfinity + ? null + : T extends JsonPrimitive + ? T + : // Instanced primitives are objects + T extends Number + ? number + : T extends String + ? string + : T extends Boolean + ? boolean + : T extends Map | Set + ? EmptyObject + : T extends TypedArray + ? Record + : T extends NotJsonable + ? never // Non-JSONable type union was found not empty + : // Any object with toJSON is special case + T extends {toJSON(): infer J} + ? (() => J) extends () => JsonValue // Is J assignable to JsonValue? + ? J // Then T is Jsonable and its Jsonable value is J + : Jsonify // Maybe if we look a level deeper we'll find a JsonValue + : T extends [] + ? [] + : T extends [unknown, ...unknown[]] + ? JsonifyTuple + : T extends ReadonlyArray + ? Array> + : T extends object + ? JsonifyObject> // JsonifyObject recursive call for its children + : never; // Otherwise any other non-object is removed diff --git a/source/set-return-type.d.ts b/source/set-return-type.d.ts index 8d6f4d8c9..03a9b79e4 100644 --- a/source/set-return-type.d.ts +++ b/source/set-return-type.d.ts @@ -1,4 +1,5 @@ -type IsAny = 0 extends (1 & T) ? true : false; // https://stackoverflow.com/a/49928360/3406963 +import type {IsAny} from './internal'; + type IsNever = [T] extends [never] ? true : false; type IsUnknown = IsNever extends false ? T extends unknown ? unknown extends T ? IsAny extends false ? true : false : false : false : false; diff --git a/test-d/jsonify.ts b/test-d/jsonify.ts index db67a2293..25e1084fd 100644 --- a/test-d/jsonify.ts +++ b/test-d/jsonify.ts @@ -312,3 +312,13 @@ type ExpectedAppDataJson = { declare const response: Jsonify; expectType(response); + +expectType({} as Jsonify); + +declare const objectWithAnyProperty: Jsonify<{ + a: any; +}>; +expectType<{a: any}>(objectWithAnyProperty); + +declare const objectWithAnyProperties: Jsonify>; +expectType>(objectWithAnyProperties);