The medium myJsonStringify handles the type-dispatch core — primitives, arrays, objects, the undefined-triple rule. This version layers on the three spec features production code depends on: a replacer (filter or transform), an indent (pretty-printing), and circular-reference detection that throws like native JSON.stringify. Defer to the medium version for the basics; this question is about layering the new features cleanly on top.
function myJsonStringifyII(
value: unknown,
replacer?: ((key: string, value: unknown) => unknown) | string[] | null,
indent?: number | string | null,
): string | undefined;
// Compact mode (same behaviour as the medium version).
myJsonStringifyII({ a: 1, b: 2 });
// '{"a":1,"b":2}'
// Replacer as an allow-list array — only listed keys survive.
myJsonStringifyII({ a: 1, b: 2, c: 3 }, ['a', 'c']);
// '{"a":1,"c":3}'
// Replacer as a function — runs for every key/value pair, return value
// is what gets serialized. Return undefined to drop a key.
myJsonStringifyII({ a: 1, b: 2 }, (key, value) =>
typeof value === 'number' ? value * 10 : value,
);
// '{"a":10,"b":20}'
// Numeric indent — newlines between entries, `indent` spaces per level.
myJsonStringifyII({ a: 1, b: [2, 3] }, null, 2);
// '{\n "a": 1,\n "b": [\n 2,\n 3\n ]\n}'
// String indent — used verbatim (clipped to 10 chars).
myJsonStringifyII({ a: 1 }, null, '\t');
// '{\n\t"a": 1\n}'
// Circular reference — throws, just like native JSON.stringify.
const a = {};
a.self = a;
myJsonStringifyII(a);
// TypeError: Converting circular structure to JSON
replacer is an array, treat it as a string allow-list applied at every object (not just the root). If it's a function, call it with (key, value) for every key/value pair, including the root keyed as the empty string "". Anything else (including null/undefined) means no filtering.value. Returning undefined drops the key from an object (or becomes "null" inside an array, mirroring the medium version's array-item rule).n in [1, 10] becomes n spaces; numbers outside that range are clipped (above 10) or ignored (zero, negative, NaN). A string is used verbatim but clipped to its first 10 characters. Anything else means compact mode.{} and [] stay compact (no inner newline) even with indent set.toJSON hook — if a value's own .toJSON is a function, call it with the key and serialize its return value instead. This runs BEFORE the replacer (matches spec ordering).BigInt (would normally throw), wrapped primitive objects (new Number(1)), and the spec's "well-known symbol" branches.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.