diff --git a/source/jsonify.d.ts b/source/jsonify.d.ts index 93e44a161..46b253d21 100644 --- a/source/jsonify.d.ts +++ b/source/jsonify.d.ts @@ -1,14 +1,9 @@ import type {JsonPrimitive, JsonValue} from './basic'; +import {Finite, NegativeInfinity, PositiveInfinity} from './numeric'; +import {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; - -// Note: Handles special case where Arrays with `undefined` are transformed to `'null'` by `JSON.stringify()` -// Only use with array members -type JsonifyArrayMember = - T extends undefined ? - null | Exclude : - Jsonify; +type NotJsonable = ((...args: any[]) => any) | undefined | symbol; /** Transform a type to one that is assignable to the `JsonValue` type. @@ -71,16 +66,26 @@ 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 JsonPrimitive + [Extract] extends [never] + ? T extends PositiveInfinity | NegativeInfinity ? null + : T extends JsonPrimitive ? T // Primitive is acceptable + : T extends Number ? number + : T extends String ? string + : T extends Boolean ? boolean + : T extends Map | Set ? {} + : T extends TypedArray ? Record : T extends Array - ? Array> // It's an array: recursive call for its children + ? Array> // It's an array: recursive call for its children : T extends object ? T extends {toJSON(): infer J} ? (() => J) extends (() => JsonValue) // Is J assignable to JsonValue? ? J // Then T is Jsonable and its Jsonable value is J : never // Not Jsonable because its toJSON() method does not return JsonValue - : {[P in keyof T]: Jsonify[P]>} // It's an object: recursive call for its children + : {[P in keyof T as P extends symbol + ? never + : T[P] extends NotJsonable + ? never + : P]: Jsonify[P]>} // It's an object: recursive call for its children : never // Otherwise any other non-object is removed : never; // Otherwise non-JSONable type union was found not empty diff --git a/source/numeric.d.ts b/source/numeric.d.ts index b0defd378..614b61a14 100644 --- a/source/numeric.d.ts +++ b/source/numeric.d.ts @@ -34,6 +34,8 @@ You can't pass a `bigint` as they are already guaranteed to be finite. Use-case: Validating and documenting parameters. +Note: This can't detect `NaN`, please upvote [this issue](https://github.com/microsoft/TypeScript/issues/28682) if you want to have this type as a built-in in TypeScript. + @example ``` import type {Finite} from 'type-fest'; diff --git a/test-d/jsonify.ts b/test-d/jsonify.ts index b9d05d8f5..19ca26fcb 100644 --- a/test-d/jsonify.ts +++ b/test-d/jsonify.ts @@ -1,5 +1,5 @@ import {expectAssignable, expectNotAssignable, expectType} from 'tsd'; -import type {Jsonify, JsonValue} from '..'; +import type {Jsonify, JsonValue, NegativeInfinity, PositiveInfinity} from '..'; interface A { a: number; @@ -123,17 +123,93 @@ const nonJsonWithInvalidToJSON = new NonJsonWithInvalidToJSON(); expectNotAssignable(nonJsonWithInvalidToJSON); expectNotAssignable(nonJsonWithInvalidToJSON.toJSON()); -// Special cases of Array with `undefined` member -// `[undefined]` is not JSON because it contains non JSON value `undefined` -// However `JSON.parse(JSON.stringify())` transforms array members of `undefined` to `null` -expectNotAssignable([undefined]); -declare const parsedStringifiedArrayWithUndefined1: Jsonify; // = JSON.parse(JSON.stringify([undefined])); -expectType(parsedStringifiedArrayWithUndefined1); -expectAssignable(parsedStringifiedArrayWithUndefined1); -expectNotAssignable([undefined, 1]); -declare const parsedStringifiedArrayWithUndefined2: Jsonify<[undefined, number]>; -expectType>(parsedStringifiedArrayWithUndefined2); -expectAssignable(parsedStringifiedArrayWithUndefined2); +// Not jsonable types; these types behave differently when used as plain values, as members of arrays and as values of objects +declare const undefined: undefined; +expectNotAssignable(undefined); + +declare const fn: (_: any) => void; +expectNotAssignable(fn); + +declare const symbol: symbol; +expectNotAssignable(symbol); + +// Plain values fail JSON.stringify() +declare const plainUndefined: Jsonify; +expectType(plainUndefined); + +declare const plainFn: Jsonify; +expectType(plainFn); + +declare const plainSymbol: Jsonify; +expectType(plainSymbol); + +// Array members become null +declare const arrayMemberUndefined: Jsonify>; +expectType(arrayMemberUndefined); + +declare const arrayMemberFn: Jsonify>; +expectType(arrayMemberFn); + +declare const arrayMemberSymbol: Jsonify>; +expectType(arrayMemberSymbol); + +// When used in object values, these keys are filtered +declare const objectValueUndefined: Jsonify<{keep: string; undefined: typeof undefined}>; +expectType<{keep: string}>(objectValueUndefined); + +declare const objectValueFn: Jsonify<{keep: string; fn: typeof fn}>; +expectType<{keep: string}>(objectValueFn); + +declare const objectValueSymbol: Jsonify<{keep: string; symbol: typeof symbol}>; +expectType<{keep: string}>(objectValueSymbol); + +// Symbol keys are filtered +declare const objectKeySymbol: Jsonify<{[key: typeof symbol]: number; keep: string}>; +expectType<{keep: string}>(objectKeySymbol); + +// Number, String and Boolean values are turned into primitive counterparts +declare const number: Number; +expectNotAssignable(number); + +declare const string: String; +expectNotAssignable(string); + +declare const boolean: Boolean; +expectNotAssignable(boolean); + +declare const numberJson: Jsonify; +expectType(numberJson); + +declare const stringJson: Jsonify; +expectType(stringJson); + +declare const booleanJson: Jsonify; +expectType(booleanJson); + +// BigInt fails JSON.stringify +declare const bigInt: Jsonify; +expectType(bigInt); + +declare const int8Array: Int8Array; +declare const int8ArrayJson: Jsonify; +expectType>(int8ArrayJson); + +declare const map: Map; +declare const mapJson: Jsonify; +expectType<{}>(mapJson); + +declare const set: Set; +declare const setJson: Jsonify; +expectType<{}>(setJson); + +// Positive and negative Infinity, NaN and null are turned into null +// NOTE: NaN is not detectable in TypeScript, so it is not tested; see https://github.com/sindresorhus/type-fest/issues/406 +declare const positiveInfinity: PositiveInfinity; +declare const positiveInfJson: Jsonify; +expectType(positiveInfJson); +declare const negativeInf: NegativeInfinity; +declare const negativeInfJson: Jsonify; +expectType(negativeInfJson); // Test that optional type members are not discarded wholesale. interface OptionalPrimitive { @@ -144,20 +220,14 @@ interface OptionalTypeUnion { a?: string | (() => any); } -interface OptionalFunction { - a?: () => any; -} - interface NonOptionalTypeUnion { a: string | undefined; } declare const jsonifiedOptionalPrimitive: Jsonify; declare const jsonifiedOptionalTypeUnion: Jsonify; -declare const jsonifiedOptionalFunction: Jsonify; declare const jsonifiedNonOptionalTypeUnion: Jsonify; expectType<{a?: string}>(jsonifiedOptionalPrimitive); expectType<{a?: never}>(jsonifiedOptionalTypeUnion); -expectType<{a?: never}>(jsonifiedOptionalFunction); expectType<{a: never}>(jsonifiedNonOptionalTypeUnion);