Skip to content

Commit

Permalink
feat(isJsonable): add isJsonable function
Browse files Browse the repository at this point in the history
  • Loading branch information
lambdalisue committed Aug 23, 2024
1 parent 1ef2706 commit 39e4eb1
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 0 deletions.
1 change: 1 addition & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"./is/function": "./is/function.ts",
"./is/instance-of": "./is/instance_of.ts",
"./is/intersection-of": "./is/intersection_of.ts",
"./is/jsonable": "./is/jsonable.ts",
"./is/literal-of": "./is/literal_of.ts",
"./is/literal-one-of": "./is/literal_one_of.ts",
"./is/map": "./is/map.ts",
Expand Down
54 changes: 54 additions & 0 deletions is/jsonable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type CustomJsonable, isCustomJsonable } from "./custom_jsonable.ts";

/**
* Represents a JSON-serializable value.
*
* See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description|Description} of `JSON.stringify()` for more information.
*/
export type Jsonable =
| string
| number
| boolean
| null
| unknown[]
| { [key: string]: unknown }
| CustomJsonable;

/**
* Returns true if `x` is a JSON-serializable value, false otherwise.
*
* It does not check array or object properties recursively.
*
* Use {@linkcode [is/custom_jsonable].isCustomJsonable|isCustomJsonable} to check if the type of `x` has a custom `toJSON` method.
*
* ```ts
* import { is, Jsonable } from "@core/unknownutil";
*
* const a: unknown = "Hello, world!";
* if (is.Jsonable(a)) {
* const _: Jsonable = a;
* }
* ```
*/
export function isJsonable(x: unknown): x is Jsonable {
switch (typeof x) {
case "undefined":
return false;
case "string":
case "number":
case "boolean":
return true;
case "bigint":
case "symbol":
case "function":
return isCustomJsonable(x);
case "object": {
if (x === null || Array.isArray(x)) return true;
const p = Object.getPrototypeOf(x);
if (p === BigInt.prototype || p === Function.prototype) {
return isCustomJsonable(x);
}
return true;
}
}
}
71 changes: 71 additions & 0 deletions is/jsonable_bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { assert } from "@std/assert";
import { isJsonable } from "./jsonable.ts";
import { buildTestcases } from "./custom_jsonable_test.ts";

const repeats = Array.from({ length: 100 });

for (const [name, value] of buildTestcases()) {
switch (name) {
case "undefined":
case "bigint":
case "function":
case "symbol":
Deno.bench({
name: "current",
fn() {
assert(repeats.every(() => !isJsonable(value)));
},
group: `isJsonable (${name})`,
});
break;
default:
Deno.bench({
name: "current",
fn() {
assert(repeats.every(() => isJsonable(value)));
},
group: `isJsonable (${name})`,
});
}
}

for (const [name, value] of buildTestcases()) {
switch (name) {
case "undefined":
case "null":
continue;
case "bigint":
case "function":
Deno.bench({
name: "current",
fn() {
const v = Object.assign(value as NonNullable<unknown>, {
toJSON: () => "custom",
});
assert(repeats.every(() => isJsonable(v)));
},
group: `isJsonable (${name} with own toJSON method)`,
});
}
}

for (const [name, value] of buildTestcases()) {
switch (name) {
case "bigint":
case "function":
Deno.bench({
name: "current",
fn() {
const proto = Object.getPrototypeOf(value);
proto.toJSON = () => "custom";
try {
assert(repeats.every(() => isJsonable(value)));
} finally {
delete proto.toJSON;
}
},
group:
`isJsonable (${name} with class prototype defines toJSON method)`,
});
}
}
136 changes: 136 additions & 0 deletions is/jsonable_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { assertEquals } from "@std/assert";
import { isJsonable } from "./jsonable.ts";
import { buildTestcases } from "./custom_jsonable_test.ts";

Deno.test("isJsonable", async (t) => {
for (const [name, value] of buildTestcases()) {
switch (name) {
case "undefined":
case "bigint":
case "function":
case "symbol":
await t.step(`return false for ${name}`, () => {
assertEquals(isJsonable(value), false);
});
break;
default:
await t.step(`return true for ${name}`, () => {
assertEquals(isJsonable(value), true);
});
}
}

for (const [name, value] of buildTestcases()) {
switch (name) {
case "undefined":
case "null":
// Skip undefined, null that is not supported by Object.assign.
continue;
case "bigint":
case "function":
// Object.assign() doesn't make bigint, function Jsonable.
await t.step(
`return false for ${name} even if it is wrapped by Object.assign()`,
() => {
assertEquals(
isJsonable(
Object.assign(value as NonNullable<unknown>, { a: 0 }),
),
false,
);
},
);
break;
default:
// Object.assign() makes other values Jsonable.
await t.step(
`return true for ${name} if it is wrapped by Object.assign()`,
() => {
assertEquals(
isJsonable(
Object.assign(value as NonNullable<unknown>, { a: 0 }),
),
true,
);
},
);
}
}

for (const [name, value] of buildTestcases()) {
switch (name) {
case "undefined":
case "null":
// Skip undefined, null that is not supported by Object.assign.
continue;
case "bigint":
case "function":
// toJSON method assigned with Object.assign() makes bigint, function Jsonable.
default:
// toJSON method assigned with Object.assign() makes other values Jsonable.
await t.step(
`return true for ${name} if it has own toJSON method`,
() => {
assertEquals(
isJsonable(
Object.assign(value as NonNullable<unknown>, {
toJSON: () => "custom",
}),
),
true,
);
},
);
}
}

for (const [name, value] of buildTestcases()) {
switch (name) {
case "undefined":
case "null":
// Skip undefined, null that does not have prototype
continue;
case "symbol":
// toJSON method defined in the class prototype does not make symbol Jsonable.
await t.step(
`return false for ${name} if the class prototype defines toJSON method`,
() => {
const proto = Object.getPrototypeOf(value);
proto.toJSON = () => "custom";
try {
assertEquals(isJsonable(value), false);
} finally {
delete proto.toJSON;
}
},
);
break;
case "bigint":
case "function":
// toJSON method defined in the class prototype makes bigint, function Jsonable.
default:
// toJSON method defined in the class prototype makes other values Jsonable.
await t.step(
`return true for ${name} if the class prototype defines toJSON method`,
() => {
const proto = Object.getPrototypeOf(value);
proto.toJSON = () => "custom";
try {
assertEquals(isJsonable(value), true);
} finally {
delete proto.toJSON;
}
},
);
}
}

await t.step(
"returns true on circular reference (unwilling behavior)",
() => {
const circular = { a: {} };
circular["a"] = circular;
assertEquals(isJsonable(circular), true);
},
);
});
20 changes: 20 additions & 0 deletions is/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isCustomJsonable } from "./custom_jsonable.ts";
import { isFunction } from "./function.ts";
import { isInstanceOf } from "./instance_of.ts";
import { isIntersectionOf } from "./intersection_of.ts";
import { isJsonable } from "./jsonable.ts";
import { isLiteralOf } from "./literal_of.ts";
import { isLiteralOneOf } from "./literal_one_of.ts";
import { isMap } from "./map.ts";
Expand Down Expand Up @@ -50,6 +51,7 @@ export * from "./custom_jsonable.ts";
export * from "./function.ts";
export * from "./instance_of.ts";
export * from "./intersection_of.ts";
export * from "./jsonable.ts";
export * from "./literal_of.ts";
export * from "./literal_one_of.ts";
export * from "./map.ts";
Expand Down Expand Up @@ -264,6 +266,23 @@ export const is: {
* ```
*/
IntersectionOf: typeof isIntersectionOf;
/**
* Returns true if `x` is a JSON-serializable value, false otherwise.
*
* It does not check array or object properties recursively.
*
* Use {@linkcode [is/custom_jsonable].isCustomJsonable|isCustomJsonable} to check if the type of `x` has a custom `toJSON` method.
*
* ```ts
* import { is, Jsonable } from "@core/unknownutil";
*
* const a: unknown = "Hello, world!";
* if (is.Jsonable(a)) {
* const _: Jsonable = a;
* }
* ```
*/
Jsonable: typeof isJsonable;
/**
* Return a type predicate function that returns `true` if the type of `x` is a literal type of `pred`.
*
Expand Down Expand Up @@ -1030,6 +1049,7 @@ export const is: {
Function: isFunction,
InstanceOf: isInstanceOf,
IntersectionOf: isIntersectionOf,
Jsonable: isJsonable,
LiteralOf: isLiteralOf,
LiteralOneOf: isLiteralOneOf,
Map: isMap,
Expand Down

0 comments on commit 39e4eb1

Please sign in to comment.