Skip to content

Commit

Permalink
Jsonify: Add support to Jsonify Non JSON that is Jsonable with toJSON()
Browse files Browse the repository at this point in the history
  • Loading branch information
darcyparker committed Aug 30, 2021
1 parent 2783a08 commit 4184e18
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 2 deletions.
8 changes: 6 additions & 2 deletions source/jsonify.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -50,6 +50,10 @@ type Jsonify<T> =
: T extends Array<infer U>
? Array<Jsonify<U>> // It's an array: recursive call for its children
: T extends object
? {[P in keyof T]: Jsonify<T[P]>} // 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<T[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
45 changes: 45 additions & 0 deletions test-d/jsonify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,48 @@ const point: Geometry = {

expectNotAssignable<JsonValue>(point);
expectAssignable<Jsonify<Geometry>>(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<Date>;
expectAssignable<string>(dateToJSON);
expectAssignable<JsonValue>(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<X> if the `@typescript-eslint/no-unsafe-assignment` eslint rule is ignored
// or an `as Jsonify<X>` 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<X>; // = JSON.parse(JSON.stringify(x));
expectAssignable<JsonValue>(parsedStringifiedX);
expectAssignable<string>(parsedStringifiedX.a);

class NonJsonWithToJSON {
public m: Map<string, number> = new Map([['a', 1], ['b', 2]]);

public toJSON(): {m: Array<[string, number]>} {
return {
m: [...this.m.entries()],
};
}
}
const nonJsonWithToJSON = new NonJsonWithToJSON();
expectNotAssignable<JsonValue>(nonJsonWithToJSON);
expectAssignable<JsonValue>(nonJsonWithToJSON.toJSON());

class NonJsonWithInvalidToJSON {
public m: Map<string, number> = 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<string, number>} {
return {
m: this.m,
};
}
}

const nonJsonWithInvalidToJSON = new NonJsonWithInvalidToJSON();
expectNotAssignable<JsonValue>(nonJsonWithInvalidToJSON);
expectNotAssignable<JsonValue>(nonJsonWithInvalidToJSON.toJSON());

0 comments on commit 4184e18

Please sign in to comment.