JSON.stringify and JSON.parse are the default way to send JavaScript values over the wire or stash them in localStorage. But JSON only knows about strings, numbers, booleans, null, plain objects, and arrays. Hand it a Date, a Map, a Set, or a BigInt and it quietly mangles them — a Date comes back as a string, a Map comes back as {}, and a BigInt makes JSON.stringify throw outright.
Implement serialize and deserialize so the round-trip preserves both structure and type. After deserialize(serialize(value)), a Date is still a Date (same instant), a Map is still a Map (same entries, same insertion order), a BigInt is still a bigint, and undefined / NaN / Infinity survive instead of being dropped or flattened to null.
// serialize: turn any supported value into a single JSON string.
function serialize(value: unknown): string;
// deserialize: turn that string back into the original value, types intact.
function deserialize(str: string): unknown;
// The contract that must hold for every supported value:
// deserialize(serialize(value)) deep-equals value
// ...and revived special values keep their type (instanceof Date, typeof bigint, ...).
The returned string must be valid JSON — JSON.parse(serialize(value)) must not throw. How you pack the type information into that string is up to you.
// A Date keeps its type and its exact instant.
const str = serialize(new Date('2026-06-04T12:30:00.000Z'));
const back = deserialize(str);
back instanceof Date; // true
back.getTime(); // 1780576200000 (same instant)
// Plain JSON would have lost this:
JSON.parse(JSON.stringify(new Date())) instanceof Date; // false — it's a string
// A nested mixture: a Map (with an object value) inside an object inside an array.
const value = [
{ label: 'config', data: new Map([['retries', 3]]), when: new Date(0) },
];
const back = deserialize(serialize(value));
back[0].data instanceof Map; // true
back[0].data.get('retries'); // 3
back[0].when instanceof Date; // true
// And the values JSON silently eats:
deserialize(serialize({ a: undefined, b: NaN, c: Infinity, d: 9n }));
// → { a: undefined, b: NaN, c: Infinity, d: 9n } (all four preserved)
Date, Map, Set, BigInt, undefined, NaN, Infinity, -Infinity, and any nesting of these inside plain objects and arrays. Primitives (string, finite number, boolean, null) pass through unchanged.Map or Set must iterate its entries in the same order they were inserted.undefined must survive everywhere — as the whole value, as an object property (the key stays present), and as an array element (the slot stays at its index).Map) — those are out of scope here, though a few appear under Going further in the solution.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.