Skip to content

Commit 3844263

Browse files
committed
feat(isJsonable): add isJsonable function
1 parent 2cca2c4 commit 3844263

File tree

6 files changed

+332
-1
lines changed

6 files changed

+332
-1
lines changed

deno.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"./is/function": "./is/function.ts",
2020
"./is/instance-of": "./is/instance_of.ts",
2121
"./is/intersection-of": "./is/intersection_of.ts",
22+
"./is/jsonable": "./is/jsonable.ts",
2223
"./is/literal-of": "./is/literal_of.ts",
2324
"./is/literal-one-of": "./is/literal_one_of.ts",
2425
"./is/map": "./is/map.ts",

is/custom_jsonable_test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { assertEquals } from "@std/assert";
22
import { isCustomJsonable } from "./custom_jsonable.ts";
33

4-
function buildTestcases(): readonly [name: string, value: unknown][] {
4+
export function buildTestcases(): readonly [name: string, value: unknown][] {
55
return [
66
["undefined", undefined],
77
["null", null],

is/jsonable.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { type CustomJsonable, isCustomJsonable } from "./custom_jsonable.ts";
2+
3+
/**
4+
* Represents a JSON-serializable value.
5+
*
6+
* See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description|Description} of `JSON.stringify()` for more information.
7+
*/
8+
export type Jsonable =
9+
| string
10+
| number
11+
| boolean
12+
| null
13+
| unknown[]
14+
| { [key: string]: unknown }
15+
| CustomJsonable;
16+
17+
/**
18+
* Returns true if `x` is a JSON-serializable value, false otherwise.
19+
*
20+
* It does not check array or object properties recursively.
21+
*
22+
* Use {@linkcode [is/custom_jsonable].isCustomJsonable|isCustomJsonable} to check if the type of `x` has a custom `toJSON` method.
23+
*
24+
* ```ts
25+
* import { is, Jsonable } from "@core/unknownutil";
26+
*
27+
* const a: unknown = "Hello, world!";
28+
* if (is.Jsonable(a)) {
29+
* const _: Jsonable = a;
30+
* }
31+
* ```
32+
*/
33+
export function isJsonable(x: unknown): x is Jsonable {
34+
switch (typeof x) {
35+
case "undefined":
36+
return false;
37+
case "string":
38+
case "number":
39+
case "boolean":
40+
return true;
41+
case "bigint":
42+
return isCustomJsonable(x);
43+
case "object": {
44+
if (x === null || Array.isArray(x)) return true;
45+
const p = Object.getPrototypeOf(x);
46+
if (p === BigInt.prototype || p === Function.prototype) {
47+
return isCustomJsonable(x);
48+
}
49+
return true;
50+
}
51+
case "symbol":
52+
case "function":
53+
return isCustomJsonable(x);
54+
}
55+
}

is/jsonable_bench.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { assert } from "@std/assert";
2+
import { isJsonable } from "./jsonable.ts";
3+
4+
const repeats = Array.from({ length: 100 });
5+
const testcases: [name: string, value: unknown][] = [
6+
undefined,
7+
null,
8+
"",
9+
0,
10+
true,
11+
[],
12+
{},
13+
0n,
14+
() => {},
15+
Symbol(),
16+
].map((x) => {
17+
const t = typeof x;
18+
switch (t) {
19+
case "object":
20+
if (x === null) {
21+
return ["null", x];
22+
} else if (Array.isArray(x)) {
23+
return ["array", x];
24+
}
25+
return ["object", x];
26+
}
27+
return [t, x];
28+
});
29+
30+
for (const [name, value] of testcases) {
31+
switch (name) {
32+
case "undefined":
33+
case "bigint":
34+
case "function":
35+
case "symbol":
36+
Deno.bench({
37+
name: "current",
38+
fn() {
39+
assert(repeats.every(() => !isJsonable(value)));
40+
},
41+
group: `isJsonable (${name})`,
42+
});
43+
break;
44+
default:
45+
Deno.bench({
46+
name: "current",
47+
fn() {
48+
assert(repeats.every(() => isJsonable(value)));
49+
},
50+
group: `isJsonable (${name})`,
51+
});
52+
}
53+
}
54+
55+
for (const [name, value] of testcases) {
56+
switch (name) {
57+
case "undefined":
58+
case "null":
59+
continue;
60+
case "bigint":
61+
case "function":
62+
Deno.bench({
63+
name: "current",
64+
fn() {
65+
const v = Object.assign(value as NonNullable<unknown>, {
66+
toJSON: () => "custom",
67+
});
68+
assert(repeats.every(() => isJsonable(v)));
69+
},
70+
group: `isJsonable (${name} with own toJSON method)`,
71+
});
72+
}
73+
}
74+
75+
for (const [name, value] of testcases) {
76+
switch (name) {
77+
case "bigint":
78+
case "function":
79+
Deno.bench({
80+
name: "current",
81+
fn() {
82+
const proto = Object.getPrototypeOf(value);
83+
proto.toJSON = () => "custom";
84+
try {
85+
assert(repeats.every(() => isJsonable(value)));
86+
} finally {
87+
delete proto.toJSON;
88+
}
89+
},
90+
group:
91+
`isJsonable (${name} with class prototype defines toJSON method)`,
92+
});
93+
}
94+
}

is/jsonable_test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { assertEquals } from "@std/assert";
2+
import { isJsonable } from "./jsonable.ts";
3+
import { buildTestcases } from "./custom_jsonable_test.ts";
4+
5+
Deno.test("isJsonable", async (t) => {
6+
for (const [name, value] of buildTestcases()) {
7+
switch (name) {
8+
case "undefined":
9+
case "bigint":
10+
case "function":
11+
case "symbol":
12+
await t.step(`return false for ${name}`, () => {
13+
assertEquals(isJsonable(value), false);
14+
});
15+
break;
16+
default:
17+
await t.step(`return true for ${name}`, () => {
18+
assertEquals(isJsonable(value), true);
19+
});
20+
}
21+
}
22+
23+
for (const [name, value] of buildTestcases()) {
24+
switch (name) {
25+
case "undefined":
26+
case "null":
27+
// Skip undefined, null that is not supported by Object.assign.
28+
continue;
29+
case "bigint":
30+
case "function":
31+
// Object.assign() doesn't make bigint, function Jsonable.
32+
await t.step(
33+
`return false for ${name} even if it is wrapped by Object.assign()`,
34+
() => {
35+
assertEquals(
36+
isJsonable(
37+
Object.assign(value as NonNullable<unknown>, { a: 0 }),
38+
),
39+
false,
40+
);
41+
},
42+
);
43+
break;
44+
default:
45+
// Object.assign() makes other values Jsonable.
46+
await t.step(
47+
`return true for ${name} if it is wrapped by Object.assign()`,
48+
() => {
49+
assertEquals(
50+
isJsonable(
51+
Object.assign(value as NonNullable<unknown>, { a: 0 }),
52+
),
53+
true,
54+
);
55+
},
56+
);
57+
}
58+
}
59+
60+
for (const [name, value] of buildTestcases()) {
61+
switch (name) {
62+
case "undefined":
63+
case "null":
64+
// Skip undefined, null that is not supported by Object.assign.
65+
continue;
66+
case "bigint":
67+
case "function":
68+
// toJSON method assigned with Object.assign() makes bigint, function Jsonable.
69+
await t.step(
70+
`return true for ${name} if it has own toJSON method`,
71+
() => {
72+
assertEquals(
73+
isJsonable(
74+
Object.assign(value as NonNullable<unknown>, {
75+
toJSON: () => "custom",
76+
}),
77+
),
78+
true,
79+
);
80+
},
81+
);
82+
break;
83+
default:
84+
// toJSON method assigned with Object.assign() makes other values Jsonable.
85+
await t.step(
86+
`return true for ${name} if it has own toJSON method`,
87+
() => {
88+
assertEquals(
89+
isJsonable(
90+
Object.assign(value as NonNullable<unknown>, {
91+
toJSON: () => "custom",
92+
}),
93+
),
94+
true,
95+
);
96+
},
97+
);
98+
}
99+
}
100+
101+
for (const [name, value] of buildTestcases()) {
102+
switch (name) {
103+
case "undefined":
104+
case "null":
105+
// Skip undefined, null that does not have prototype
106+
continue;
107+
case "bigint":
108+
case "function":
109+
// toJSON method defined in the class prototype makes bigint, function Jsonable.
110+
await t.step(
111+
`return true for ${name} if the class prototype defines toJSON method`,
112+
() => {
113+
const proto = Object.getPrototypeOf(value);
114+
proto.toJSON = () => "custom";
115+
try {
116+
assertEquals(isJsonable(value), true);
117+
} finally {
118+
delete proto.toJSON;
119+
}
120+
},
121+
);
122+
break;
123+
case "symbol":
124+
// toJSON method defined in the class prototype does not make symbol Jsonable.
125+
await t.step(
126+
`return false for ${name} if the class prototype defines toJSON method`,
127+
() => {
128+
const proto = Object.getPrototypeOf(value);
129+
proto.toJSON = () => "custom";
130+
try {
131+
assertEquals(isJsonable(value), false);
132+
} finally {
133+
delete proto.toJSON;
134+
}
135+
},
136+
);
137+
break;
138+
default:
139+
// toJSON method defined in the class prototype makes other values Jsonable.
140+
await t.step(
141+
`return true for ${name} if the class prototype defines toJSON method`,
142+
() => {
143+
const proto = Object.getPrototypeOf(value);
144+
proto.toJSON = () => "custom";
145+
try {
146+
assertEquals(isJsonable(value), true);
147+
} finally {
148+
delete proto.toJSON;
149+
}
150+
},
151+
);
152+
}
153+
}
154+
155+
await t.step(
156+
"returns true on circular reference (unwilling behavior)",
157+
() => {
158+
const circular = { a: {} };
159+
circular["a"] = circular;
160+
assertEquals(isJsonable(circular), true);
161+
},
162+
);
163+
});

is/mod.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isCustomJsonable } from "./custom_jsonable.ts";
99
import { isFunction } from "./function.ts";
1010
import { isInstanceOf } from "./instance_of.ts";
1111
import { isIntersectionOf } from "./intersection_of.ts";
12+
import { isJsonable } from "./jsonable.ts";
1213
import { isLiteralOf } from "./literal_of.ts";
1314
import { isLiteralOneOf } from "./literal_one_of.ts";
1415
import { isMap } from "./map.ts";
@@ -50,6 +51,7 @@ export * from "./custom_jsonable.ts";
5051
export * from "./function.ts";
5152
export * from "./instance_of.ts";
5253
export * from "./intersection_of.ts";
54+
export * from "./jsonable.ts";
5355
export * from "./literal_of.ts";
5456
export * from "./literal_one_of.ts";
5557
export * from "./map.ts";
@@ -264,6 +266,21 @@ export const is: {
264266
* ```
265267
*/
266268
IntersectionOf: typeof isIntersectionOf;
269+
/**
270+
* Returns true if `x` is a JSON-serializable value, false otherwise.
271+
*
272+
* Use {@linkcode [is/custom_jsonable].isCustomJsonable|isCustomJsonable} to check if the type of `x` has a custom `toJSON` method.
273+
*
274+
* ```ts
275+
* import { is, Jsonable } from "@core/unknownutil";
276+
*
277+
* const a: unknown = "Hello, world!";
278+
* if (is.Jsonable(a)) {
279+
* const _: Jsonable = a;
280+
* }
281+
* ```
282+
*/
283+
Jsonable: typeof isJsonable;
267284
/**
268285
* Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`.
269286
*
@@ -1030,6 +1047,7 @@ export const is: {
10301047
Function: isFunction,
10311048
InstanceOf: isInstanceOf,
10321049
IntersectionOf: isIntersectionOf,
1050+
Jsonable: isJsonable,
10331051
LiteralOf: isLiteralOf,
10341052
LiteralOneOf: isLiteralOneOf,
10351053
Map: isMap,

0 commit comments

Comments
 (0)