From 4184e180acc46df51c13cd99a341a7d3722942fb Mon Sep 17 00:00:00 2001 From: Darcy Parker Date: Mon, 30 Aug 2021 10:58:42 -0400 Subject: [PATCH 1/6] Jsonify: Add support to Jsonify Non JSON that is Jsonable with toJSON() --- source/jsonify.d.ts | 8 ++++++-- test-d/jsonify.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/source/jsonify.d.ts b/source/jsonify.d.ts index 80b50c773..0e36e159a 100644 --- a/source/jsonify.d.ts +++ b/source/jsonify.d.ts @@ -1,4 +1,4 @@ -import {JsonPrimitive} from './basic'; +import {JsonPrimitive, JsonValue} from './basic'; // Note: The return value has to be `any` and not `unknown` so it can match `void`. type NotJsonable = ((...args: any[]) => any) | undefined; @@ -50,6 +50,10 @@ type Jsonify = : T extends Array ? Array> // It's an array: recursive call for its children : T extends object - ? {[P in keyof T]: Jsonify} // It's an object: recursive call for its children + ? 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} // 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/test-d/jsonify.ts b/test-d/jsonify.ts index 837de3ecb..24c03776b 100644 --- a/test-d/jsonify.ts +++ b/test-d/jsonify.ts @@ -76,3 +76,48 @@ const point: Geometry = { expectNotAssignable(point); expectAssignable>(point); + +// The following const values are examples of values `v` that are not JSON, but are Jsonable using +// `v.toJSON()` or `JSON.parse(JSON.stringify(v))` +declare const dateToJSON: Jsonify; +expectAssignable(dateToJSON); +expectAssignable(dateToJSON); + +// The following commented `= JSON.parse(JSON.stringify(x))` is an example of how `parsedStringifiedX` could be created. +// * Note that this would be an unsafe assignment that needs because `JSON.parse()` returns type `any`. +// But by inspection `JSON.stringify(x)` will use `x.a.toJSON()`. So the JSON.parse() result can be +// assigned to Jsonify if the `@typescript-eslint/no-unsafe-assignment` eslint rule is ignored +// or an `as Jsonify` is added. +// * This test is not about how `parsedStringifiedX` is created, but about its type, so the `const` value is declared +declare const parsedStringifiedX: Jsonify; // = JSON.parse(JSON.stringify(x)); +expectAssignable(parsedStringifiedX); +expectAssignable(parsedStringifiedX.a); + +class NonJsonWithToJSON { + public m: Map = new Map([['a', 1], ['b', 2]]); + + public toJSON(): {m: Array<[string, number]>} { + return { + m: [...this.m.entries()], + }; + } +} +const nonJsonWithToJSON = new NonJsonWithToJSON(); +expectNotAssignable(nonJsonWithToJSON); +expectAssignable(nonJsonWithToJSON.toJSON()); + +class NonJsonWithInvalidToJSON { + public m: Map = new Map([['a', 1], ['b', 2]]); + + // This is intentionally invalid toJSON() + // It is invalid because the result is not assignable to JsonValue + public toJSON(): {m: Map} { + return { + m: this.m, + }; + } +} + +const nonJsonWithInvalidToJSON = new NonJsonWithInvalidToJSON(); +expectNotAssignable(nonJsonWithInvalidToJSON); +expectNotAssignable(nonJsonWithInvalidToJSON.toJSON()); From 9397d7a7c4943b8f0ed0147221d47aaddc193822 Mon Sep 17 00:00:00 2001 From: Darcy Parker Date: Mon, 30 Aug 2021 11:39:19 -0400 Subject: [PATCH 2/6] Fix comment --- test-d/jsonify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-d/jsonify.ts b/test-d/jsonify.ts index 24c03776b..47a0011a1 100644 --- a/test-d/jsonify.ts +++ b/test-d/jsonify.ts @@ -84,7 +84,7 @@ expectAssignable(dateToJSON); expectAssignable(dateToJSON); // The following commented `= JSON.parse(JSON.stringify(x))` is an example of how `parsedStringifiedX` could be created. -// * Note that this would be an unsafe assignment that needs because `JSON.parse()` returns type `any`. +// * Note that this would be an unsafe assignment because `JSON.parse()` returns type `any`. // But by inspection `JSON.stringify(x)` will use `x.a.toJSON()`. So the JSON.parse() result can be // assigned to Jsonify if the `@typescript-eslint/no-unsafe-assignment` eslint rule is ignored // or an `as Jsonify` is added. From 43acdbedd2eb149c6b8120ceb19a808d02b5f3c7 Mon Sep 17 00:00:00 2001 From: Darcy Parker Date: Mon, 30 Aug 2021 11:43:57 -0400 Subject: [PATCH 3/6] Add additional expectation to test --- test-d/jsonify.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test-d/jsonify.ts b/test-d/jsonify.ts index 47a0011a1..6f0505b8f 100644 --- a/test-d/jsonify.ts +++ b/test-d/jsonify.ts @@ -105,6 +105,7 @@ class NonJsonWithToJSON { const nonJsonWithToJSON = new NonJsonWithToJSON(); expectNotAssignable(nonJsonWithToJSON); expectAssignable(nonJsonWithToJSON.toJSON()); +expectAssignable>(nonJsonWithToJSON.toJSON()); class NonJsonWithInvalidToJSON { public m: Map = new Map([['a', 1], ['b', 2]]); From 2a54d6050a5136fd1b349ea6c8a6db5094aa7d4c Mon Sep 17 00:00:00 2001 From: Darcy Parker Date: Tue, 7 Sep 2021 10:28:34 -0400 Subject: [PATCH 4/6] jsonify.d.ts: Update documentation --- source/jsonify.d.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/source/jsonify.d.ts b/source/jsonify.d.ts index 0e36e159a..08268e5f1 100644 --- a/source/jsonify.d.ts +++ b/source/jsonify.d.ts @@ -4,7 +4,10 @@ import {JsonPrimitive, JsonValue} from './basic'; type NotJsonable = ((...args: any[]) => any) | undefined; /** -Transform a type to one that is assignable to the `JsonValue` type. +Transform a type to one that is assignable to the `JsonValue` type. This includes: + +1. Transforming JSON `interface` to a `type` that is assignable to `JsonValue` +2. Transforming non JSON that is **Jsonable**, to a type that is assignable to JsonValue. Where **Jsonable** means the non JSON value implements `.toJSON()` method that returns a value that is assignable to `JsonValue`. @remarks @@ -36,6 +39,17 @@ fixedFn(point); // Good: point is assignable. Jsonify transforms Geometry int fixedFn(new Date()); // Error: As expected, Date is not assignable. Jsonify cannot transforms Date into value assignable to JsonValue ``` +Non-JSON values such as `Date` implement `.toJSON()`, so they can be transformed to a value assignable to `JsonValue`. + +@example +``` +const a = { + timeValue: new Date() +}; +//Below, `Jsonify` is equivalent to `{timeValue: string}` +const aJson = JSON.parse(JSON.stringify(a)) as Jsonify; +``` + @link https://github.com/Microsoft/TypeScript/issues/1897#issuecomment-710744173 @category Utilities From b3ee3300104bf5499621a7d21269525d222852cc Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 10 Sep 2021 11:00:11 +0700 Subject: [PATCH 5/6] Update jsonify.d.ts --- source/jsonify.d.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/source/jsonify.d.ts b/source/jsonify.d.ts index 08268e5f1..bc69492a8 100644 --- a/source/jsonify.d.ts +++ b/source/jsonify.d.ts @@ -4,10 +4,11 @@ import {JsonPrimitive, JsonValue} from './basic'; type NotJsonable = ((...args: any[]) => any) | undefined; /** -Transform a type to one that is assignable to the `JsonValue` type. This includes: +Transform a type to one that is assignable to the `JsonValue` type. -1. Transforming JSON `interface` to a `type` that is assignable to `JsonValue` -2. Transforming non JSON that is **Jsonable**, to a type that is assignable to JsonValue. Where **Jsonable** means the non JSON value implements `.toJSON()` method that returns a value that is assignable to `JsonValue`. +This includes: +1. Transforming JSON `interface` to a `type` that is assignable to `JsonValue`. +2. Transforming non-JSON value that is *jsonable* to a type that is assignable to `JsonValue`, where *jsonable* means the non-JSON value implements the `.toJSON()` method that returns a value that is assignable to `JsonValue`. @remarks @@ -39,15 +40,16 @@ fixedFn(point); // Good: point is assignable. Jsonify transforms Geometry int fixedFn(new Date()); // Error: As expected, Date is not assignable. Jsonify cannot transforms Date into value assignable to JsonValue ``` -Non-JSON values such as `Date` implement `.toJSON()`, so they can be transformed to a value assignable to `JsonValue`. +Non-JSON values such as `Date` implement `.toJSON()`, so they can be transformed to a value assignable to `JsonValue`: @example ``` -const a = { +const time = { timeValue: new Date() }; -//Below, `Jsonify` is equivalent to `{timeValue: string}` -const aJson = JSON.parse(JSON.stringify(a)) as Jsonify; + +// `Jsonify` is equivalent to `{timeValue: string}` +const timeJson = JSON.parse(JSON.stringify(time)) as Jsonify; ``` @link https://github.com/Microsoft/TypeScript/issues/1897#issuecomment-710744173 From ea5e477f57cc525b55d9b2f324080972581b9cd6 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 10 Sep 2021 11:03:25 +0700 Subject: [PATCH 6/6] Update jsonify.ts --- test-d/jsonify.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test-d/jsonify.ts b/test-d/jsonify.ts index 6f0505b8f..691fdcd08 100644 --- a/test-d/jsonify.ts +++ b/test-d/jsonify.ts @@ -77,7 +77,7 @@ const point: Geometry = { expectNotAssignable(point); expectAssignable>(point); -// The following const values are examples of values `v` that are not JSON, but are Jsonable using +// The following const values are examples of values `v` that are not JSON, but are *jsonable* using // `v.toJSON()` or `JSON.parse(JSON.stringify(v))` declare const dateToJSON: Jsonify; expectAssignable(dateToJSON); @@ -85,20 +85,20 @@ expectAssignable(dateToJSON); // The following commented `= JSON.parse(JSON.stringify(x))` is an example of how `parsedStringifiedX` could be created. // * Note that this would be an unsafe assignment because `JSON.parse()` returns type `any`. -// But by inspection `JSON.stringify(x)` will use `x.a.toJSON()`. So the JSON.parse() result can be -// assigned to Jsonify if the `@typescript-eslint/no-unsafe-assignment` eslint rule is ignored +// But by inspection `JSON.stringify(x)` will use `x.a.toJSON()`. So the `JSON.parse()` result can be +// assigned to `Jsonify` if the `@typescript-eslint/no-unsafe-assignment` ESLint rule is ignored // or an `as Jsonify` is added. -// * This test is not about how `parsedStringifiedX` is created, but about its type, so the `const` value is declared +// * This test is not about how `parsedStringifiedX` is created, but about its type, so the `const` value is declared. declare const parsedStringifiedX: Jsonify; // = JSON.parse(JSON.stringify(x)); expectAssignable(parsedStringifiedX); expectAssignable(parsedStringifiedX.a); class NonJsonWithToJSON { - public m: Map = new Map([['a', 1], ['b', 2]]); + public fixture: Map = new Map([['a', 1], ['b', 2]]); - public toJSON(): {m: Array<[string, number]>} { + public toJSON(): {fixture: Array<[string, number]>} { return { - m: [...this.m.entries()], + fixture: [...this.fixture.entries()], }; } } @@ -108,13 +108,13 @@ expectAssignable(nonJsonWithToJSON.toJSON()); expectAssignable>(nonJsonWithToJSON.toJSON()); class NonJsonWithInvalidToJSON { - public m: Map = new Map([['a', 1], ['b', 2]]); + public fixture: Map = new Map([['a', 1], ['b', 2]]); - // This is intentionally invalid toJSON() - // It is invalid because the result is not assignable to JsonValue - public toJSON(): {m: Map} { + // This is intentionally invalid `.toJSON()`. + // It is invalid because the result is not assignable to `JsonValue`. + public toJSON(): {fixture: Map} { return { - m: this.m, + fixture: this.fixture, }; } }