Implement deepClone(value) — return a new value that has the same structure as the input but shares no references with it. Mutating the clone (or anything nested inside it) must never reach back and touch the original. This is the function structuredClone gives you for free in modern runtimes, but the point of the exercise is to build it yourself: handle nested objects and arrays, detect cycles, and decide what to do about types that don't survive a JSON round-trip.
// Returns a deep, structural copy of `value`. The result shares no object
// references with the input — mutating the result never mutates the input.
function deepClone<T>(value: T): T;
const original = { name: 'Ada', address: { city: 'London' } };
const copy = deepClone(original);
copy.address.city = 'Paris';
original.address.city; // 'London' — original is untouched
copy === original; // false — top-level reference is new
copy.address === original.address; // false — nested reference is new
// Arrays clone recursively.
const arr = [[1, 2], [3, 4]];
const cloned = deepClone(arr);
cloned[0].push(99);
arr[0]; // [1, 2] — original inner array is untouched
// Cycles are handled — no stack overflow.
const a = { name: 'a' };
a.self = a; // a points at itself
const b = deepClone(a);
b.self === b; // true — cycle is preserved, but to the *clone*
b.self === a; // false — never references the original
// Primitives come back verbatim.
deepClone(42); // 42
deepClone('hello'); // 'hello'
deepClone(null); // null
deepClone(undefined); // undefined
WeakMap keyed by seen source object to map each original to its clone.Array.isArray(value) distinguishes the two — they need different containers ([] vs {}) but the same recursive treatment.Object.keys — don't copy the prototype chain, don't copy inherited properties.null is its own thing. typeof null === 'object' but null has no properties. Return it verbatim, not {}.JSON.parse(JSON.stringify(...)). It looks tempting but silently drops undefined, turns Date into a string, throws on cycles, and erases functions and Symbols. See the solution's "Going further" for what a fuller implementation would add.You'll write a function that walks a nested value and returns a new value with the same shape — every object, every array, every nested level — without sharing a single reference with the input.
Picture a configuration object you got from a parent component. You want to tweak a deep field — config.user.preferences.theme = 'dark' — without the parent ever seeing your edit. If you mutate the original, the parent sees it. If you do { ...config } you copy the outer shell, but config.user.preferences is still the same object as the parent's: change theme on the clone, the parent's theme flips too. That sharing-by-reference is exactly what deep clone has to break, at every level of nesting.
A JS value is either a primitive (a number, string, boolean, null, undefined) or a reference to an object/array sitting somewhere in memory. Primitives are passed by value — no aliasing risk. References are the danger. Deep clone is a tree walk: at every container, allocate a fresh {} or [], then recurse on each value to fill it in. Primitives are the base case — return them as-is, since they can't be aliased anyway.
The deep clone has to allocate a new object at every level the recursion descends into.
The one-liner everyone reaches for first:
function naiveClone(value) {
return JSON.parse(JSON.stringify(value));
}
For a tree of strings and numbers, this works. Then you hit a real input. JSON.stringify has a list of values it doesn't know what to do with, and its decision for each one is "silently corrupt the data." Try this:
const input = {
name: 'Ada',
joined: new Date(0), // becomes a string
tags: undefined, // key disappears entirely
score: NaN, // becomes null
};
naiveClone(input);
// { name: 'Ada', joined: '1970-01-01T00:00:00.000Z', score: null }
// ^^^ joined is a string now, tags is gone, score is null.
And the worst one — give it a cycle:
const a = { name: 'a' };
a.self = a;
naiveClone(a);
// TypeError: Converting circular structure to JSON
Not a quiet bug, an outright throw. Any object graph with a back-edge (which is common in React state, in the DOM, in any linked structure) crashes the function.
We can do better with a small recursive walk and a WeakMap to remember objects we've already cloned.
function deepClone(value, seen = new WeakMap()) {
// Base case: primitives (number, string, boolean, bigint, symbol) AND null.
// `typeof null === 'object'` so the explicit null check has to come first,
// otherwise we'd fall through and try to treat null like an object.
if (value === null || typeof value !== 'object') {
return value;
}
// Cycle guard. If we've already cloned this exact source object, return
// the clone we made — don't recurse into it again. This is what stops
// a self-referencing input from blowing the stack.
if (seen.has(value)) {
return seen.get(value);
}
// Arrays and plain objects need different containers. Decide which to
// allocate, then register THIS source -> THIS clone in `seen` BEFORE we
// recurse. The "before" is critical: a child might cycle back to `value`,
// and the cache lookup above only works if the entry is already there.
const clone = Array.isArray(value) ? [] : {};
seen.set(value, clone);
// Walk the source's own enumerable keys. `Object.keys` skips inherited
// properties (the prototype chain) and non-enumerable own properties —
// both desirable. For arrays, `Object.keys` returns the indices as strings;
// assigning to `clone[key]` works the same for `[]` and `{}`.
for (const key of Object.keys(value)) {
clone[key] = deepClone(value[key], seen);
}
return clone;
}
module.exports = { deepClone };
Three shifts from the naive version. First, primitives short-circuit. Numbers, strings, booleans, null, and undefined can't be aliased — there's nothing to clone — so they're the base case of the recursion. Second, we allocate a new container at each level ([] for arrays, {} for everything else) and assign cloned values into it. The new allocation is what guarantees clone.address !== original.address. Third, the WeakMap makes cycles safe: before we recurse into the children, we record value -> clone, so if a child reaches back up and asks "have we seen this?" the answer is "yes, here it is" and the recursion terminates.
A note on WeakMap over Map: keys in a WeakMap are held weakly, so once the recursion finishes and the function returns, the map and all its key-clones become garbage-collectible. A regular Map works functionally too, but WeakMap is the conventional choice for "remember object identities only as long as they exist."
Trace deepClone(input) for this input — three levels deep, with one array, mutation safety, and a cycle for good measure:
const input = { name: 'Ada', address: { city: 'London' }, tags: ['admin'] };
input.self = input; // cycle: input.self === input
const out = deepClone(input);
Step by step:
deepClone(input, seen={}). input is not null, typeof input === 'object'. seen.has(input) is false. Array.isArray(input) is false, so clone = {}. seen.set(input, clone) — the map now holds { input -> clone }.Object.keys(input) -> ['name', 'address', 'tags', 'self'].'name'. Recurse: deepClone('Ada', seen). 'Ada' is a string — base case — returns 'Ada'. Assign clone.name = 'Ada'.'address'. Recurse: deepClone({ city: 'London' }, seen). Not primitive. seen.has is false (this address object is new). Allocate addressClone = {}. seen.set(input.address, addressClone). Recurse on key 'city' — base case, returns 'London'. addressClone.city = 'London'. Return addressClone. Back in the outer call: clone.address = addressClone (a brand-new object, not input.address).'tags'. Recurse: deepClone(['admin'], seen). Not primitive. seen.has is false. Array.isArray is true, so allocate tagsClone = []. seen.set(input.tags, tagsClone). Iterate keys of ['admin'] — that's ['0']. Recurse on 'admin' — base case, returns 'admin'. tagsClone[0] = 'admin'. Return tagsClone. Back outside: clone.tags = tagsClone.'self'. Recurse: deepClone(input, seen). Not primitive. seen.has(input) is TRUE — we set that on entry in step 1. Return seen.get(input) — which is clone itself. Back outside: clone.self = clone. The cycle is preserved, but it lives entirely inside the clone graph.clone.After all this:
out === input; // false — top-level reference is new
out.address === input.address; // false — nested reference is new
out.tags === input.tags; // false — nested array is new
out.self === out; // true — cycle preserved within the clone
out.self === input; // false — never reaches back to the source
out.address.city = 'Paris';
input.address.city; // 'London' — original is untouched
The recursion depth equals the depth of the input tree — usually fine. For a degenerately deep structure (a thousand levels), an iterative version with an explicit stack would be safer; see "Going further."
typeof null === 'object'. If you write if (typeof value !== 'object') return value; without an explicit null check first, the function falls into the recursive branch on null, tries Object.keys(null), and throws TypeError: Cannot convert undefined or null to object. Always handle null before the typeof check.for (...) clone[key] = deepClone(value[key], seen); seen.set(value, clone); — note the set is at the bottom — the cycle guard fails. The first time a child reaches back to value, seen.has(value) is still false; the recursion descends again and you stack-overflow. seen.set(value, clone) MUST come before the loop.for...in instead of Object.keys. for...in walks the prototype chain. If a caller does Object.create({ inherited: 'x' }) and adds an own property, your clone would copy inherited too — which is wrong (it's not an own property of the source) and surprising (the clone has properties the source doesn't have when you check with hasOwnProperty).{} and assign by key, an input of [1, 2, 3] becomes the object { '0': 1, '1': 2, '2': 3 }. Array.isArray(cloned) returns false; cloned.length is undefined. The Array.isArray(value) ? [] : {} choice is what keeps arrays as arrays.Map instead of WeakMap for the cycle cache. A regular Map works functionally — same has/get/set — but holds strong references to every cloned source. If deepClone is called inside a long-running scope and the input objects would otherwise be garbage-collected, the Map keeps them alive until the map itself is dropped. WeakMap lets them be collected as soon as the recursion returns.Date, RegExp, Map, and Set. Each is a typed object that needs its own clone path: new Date(value.getTime()) for dates, new RegExp(value.source, value.flags) for regexes, and new Map([...value]) / new Set([...value]) with their entries recursively cloned. The dispatch is a chain of instanceof checks before the plain-object fallback. (Date is the most common one to get wrong — JSON.stringify mangles it to a string, and people copy-paste that behavior into their own clones without noticing.)structuredClone when it's available. Modern Node (≥ 17) and modern browsers ship structuredClone(value) as a built-in. It handles cycles, Date, Map, Set, TypedArray, and more — but it still throws on functions and DOM nodes, and it can't be customized. The hand-rolled version above is the right answer for an interview; for production code the built-in is one line and faster.{ source, clone, key } triples onto an array and loop until empty. The control flow is uglier — you can't just return a finished clone, you have to assign it into its parent — but the depth limit goes away.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement deepClone(value) — return a new value that has the same structure as the input but shares no references with it. Mutating the clone (or anything nested inside it) must never reach back and touch the original. This is the function structuredClone gives you for free in modern runtimes, but the point of the exercise is to build it yourself: handle nested objects and arrays, detect cycles, and decide what to do about types that don't survive a JSON round-trip.
// Returns a deep, structural copy of `value`. The result shares no object
// references with the input — mutating the result never mutates the input.
function deepClone<T>(value: T): T;
const original = { name: 'Ada', address: { city: 'London' } };
const copy = deepClone(original);
copy.address.city = 'Paris';
original.address.city; // 'London' — original is untouched
copy === original; // false — top-level reference is new
copy.address === original.address; // false — nested reference is new
// Arrays clone recursively.
const arr = [[1, 2], [3, 4]];
const cloned = deepClone(arr);
cloned[0].push(99);
arr[0]; // [1, 2] — original inner array is untouched
// Cycles are handled — no stack overflow.
const a = { name: 'a' };
a.self = a; // a points at itself
const b = deepClone(a);
b.self === b; // true — cycle is preserved, but to the *clone*
b.self === a; // false — never references the original
// Primitives come back verbatim.
deepClone(42); // 42
deepClone('hello'); // 'hello'
deepClone(null); // null
deepClone(undefined); // undefined
WeakMap keyed by seen source object to map each original to its clone.Array.isArray(value) distinguishes the two — they need different containers ([] vs {}) but the same recursive treatment.Object.keys — don't copy the prototype chain, don't copy inherited properties.null is its own thing. typeof null === 'object' but null has no properties. Return it verbatim, not {}.JSON.parse(JSON.stringify(...)). It looks tempting but silently drops undefined, turns Date into a string, throws on cycles, and erases functions and Symbols. See the solution's "Going further" for what a fuller implementation would add.You'll write a function that walks a nested value and returns a new value with the same shape — every object, every array, every nested level — without sharing a single reference with the input.
Picture a configuration object you got from a parent component. You want to tweak a deep field — config.user.preferences.theme = 'dark' — without the parent ever seeing your edit. If you mutate the original, the parent sees it. If you do { ...config } you copy the outer shell, but config.user.preferences is still the same object as the parent's: change theme on the clone, the parent's theme flips too. That sharing-by-reference is exactly what deep clone has to break, at every level of nesting.
A JS value is either a primitive (a number, string, boolean, null, undefined) or a reference to an object/array sitting somewhere in memory. Primitives are passed by value — no aliasing risk. References are the danger. Deep clone is a tree walk: at every container, allocate a fresh {} or [], then recurse on each value to fill it in. Primitives are the base case — return them as-is, since they can't be aliased anyway.
The deep clone has to allocate a new object at every level the recursion descends into.
The one-liner everyone reaches for first:
function naiveClone(value) {
return JSON.parse(JSON.stringify(value));
}
For a tree of strings and numbers, this works. Then you hit a real input. JSON.stringify has a list of values it doesn't know what to do with, and its decision for each one is "silently corrupt the data." Try this:
const input = {
name: 'Ada',
joined: new Date(0), // becomes a string
tags: undefined, // key disappears entirely
score: NaN, // becomes null
};
naiveClone(input);
// { name: 'Ada', joined: '1970-01-01T00:00:00.000Z', score: null }
// ^^^ joined is a string now, tags is gone, score is null.
And the worst one — give it a cycle:
const a = { name: 'a' };
a.self = a;
naiveClone(a);
// TypeError: Converting circular structure to JSON
Not a quiet bug, an outright throw. Any object graph with a back-edge (which is common in React state, in the DOM, in any linked structure) crashes the function.
We can do better with a small recursive walk and a WeakMap to remember objects we've already cloned.
function deepClone(value, seen = new WeakMap()) {
// Base case: primitives (number, string, boolean, bigint, symbol) AND null.
// `typeof null === 'object'` so the explicit null check has to come first,
// otherwise we'd fall through and try to treat null like an object.
if (value === null || typeof value !== 'object') {
return value;
}
// Cycle guard. If we've already cloned this exact source object, return
// the clone we made — don't recurse into it again. This is what stops
// a self-referencing input from blowing the stack.
if (seen.has(value)) {
return seen.get(value);
}
// Arrays and plain objects need different containers. Decide which to
// allocate, then register THIS source -> THIS clone in `seen` BEFORE we
// recurse. The "before" is critical: a child might cycle back to `value`,
// and the cache lookup above only works if the entry is already there.
const clone = Array.isArray(value) ? [] : {};
seen.set(value, clone);
// Walk the source's own enumerable keys. `Object.keys` skips inherited
// properties (the prototype chain) and non-enumerable own properties —
// both desirable. For arrays, `Object.keys` returns the indices as strings;
// assigning to `clone[key]` works the same for `[]` and `{}`.
for (const key of Object.keys(value)) {
clone[key] = deepClone(value[key], seen);
}
return clone;
}
module.exports = { deepClone };
Three shifts from the naive version. First, primitives short-circuit. Numbers, strings, booleans, null, and undefined can't be aliased — there's nothing to clone — so they're the base case of the recursion. Second, we allocate a new container at each level ([] for arrays, {} for everything else) and assign cloned values into it. The new allocation is what guarantees clone.address !== original.address. Third, the WeakMap makes cycles safe: before we recurse into the children, we record value -> clone, so if a child reaches back up and asks "have we seen this?" the answer is "yes, here it is" and the recursion terminates.
A note on WeakMap over Map: keys in a WeakMap are held weakly, so once the recursion finishes and the function returns, the map and all its key-clones become garbage-collectible. A regular Map works functionally too, but WeakMap is the conventional choice for "remember object identities only as long as they exist."
Trace deepClone(input) for this input — three levels deep, with one array, mutation safety, and a cycle for good measure:
const input = { name: 'Ada', address: { city: 'London' }, tags: ['admin'] };
input.self = input; // cycle: input.self === input
const out = deepClone(input);
Step by step:
deepClone(input, seen={}). input is not null, typeof input === 'object'. seen.has(input) is false. Array.isArray(input) is false, so clone = {}. seen.set(input, clone) — the map now holds { input -> clone }.Object.keys(input) -> ['name', 'address', 'tags', 'self'].'name'. Recurse: deepClone('Ada', seen). 'Ada' is a string — base case — returns 'Ada'. Assign clone.name = 'Ada'.'address'. Recurse: deepClone({ city: 'London' }, seen). Not primitive. seen.has is false (this address object is new). Allocate addressClone = {}. seen.set(input.address, addressClone). Recurse on key 'city' — base case, returns 'London'. addressClone.city = 'London'. Return addressClone. Back in the outer call: clone.address = addressClone (a brand-new object, not input.address).'tags'. Recurse: deepClone(['admin'], seen). Not primitive. seen.has is false. Array.isArray is true, so allocate tagsClone = []. seen.set(input.tags, tagsClone). Iterate keys of ['admin'] — that's ['0']. Recurse on 'admin' — base case, returns 'admin'. tagsClone[0] = 'admin'. Return tagsClone. Back outside: clone.tags = tagsClone.'self'. Recurse: deepClone(input, seen). Not primitive. seen.has(input) is TRUE — we set that on entry in step 1. Return seen.get(input) — which is clone itself. Back outside: clone.self = clone. The cycle is preserved, but it lives entirely inside the clone graph.clone.After all this:
out === input; // false — top-level reference is new
out.address === input.address; // false — nested reference is new
out.tags === input.tags; // false — nested array is new
out.self === out; // true — cycle preserved within the clone
out.self === input; // false — never reaches back to the source
out.address.city = 'Paris';
input.address.city; // 'London' — original is untouched
The recursion depth equals the depth of the input tree — usually fine. For a degenerately deep structure (a thousand levels), an iterative version with an explicit stack would be safer; see "Going further."
typeof null === 'object'. If you write if (typeof value !== 'object') return value; without an explicit null check first, the function falls into the recursive branch on null, tries Object.keys(null), and throws TypeError: Cannot convert undefined or null to object. Always handle null before the typeof check.for (...) clone[key] = deepClone(value[key], seen); seen.set(value, clone); — note the set is at the bottom — the cycle guard fails. The first time a child reaches back to value, seen.has(value) is still false; the recursion descends again and you stack-overflow. seen.set(value, clone) MUST come before the loop.for...in instead of Object.keys. for...in walks the prototype chain. If a caller does Object.create({ inherited: 'x' }) and adds an own property, your clone would copy inherited too — which is wrong (it's not an own property of the source) and surprising (the clone has properties the source doesn't have when you check with hasOwnProperty).{} and assign by key, an input of [1, 2, 3] becomes the object { '0': 1, '1': 2, '2': 3 }. Array.isArray(cloned) returns false; cloned.length is undefined. The Array.isArray(value) ? [] : {} choice is what keeps arrays as arrays.Map instead of WeakMap for the cycle cache. A regular Map works functionally — same has/get/set — but holds strong references to every cloned source. If deepClone is called inside a long-running scope and the input objects would otherwise be garbage-collected, the Map keeps them alive until the map itself is dropped. WeakMap lets them be collected as soon as the recursion returns.Date, RegExp, Map, and Set. Each is a typed object that needs its own clone path: new Date(value.getTime()) for dates, new RegExp(value.source, value.flags) for regexes, and new Map([...value]) / new Set([...value]) with their entries recursively cloned. The dispatch is a chain of instanceof checks before the plain-object fallback. (Date is the most common one to get wrong — JSON.stringify mangles it to a string, and people copy-paste that behavior into their own clones without noticing.)structuredClone when it's available. Modern Node (≥ 17) and modern browsers ship structuredClone(value) as a built-in. It handles cycles, Date, Map, Set, TypedArray, and more — but it still throws on functions and DOM nodes, and it can't be customized. The hand-rolled version above is the right answer for an interview; for production code the built-in is one line and faster.{ source, clone, key } triples onto an array and loop until empty. The control flow is uglier — you can't just return a finished clone, you have to assign it into its parent — but the depth limit goes away.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.