← Back to question bank
JS FunctionMidMedium#2013 · 30m

Implement deep clone with cycles

Build deep clone with cycles. The interviewer expects a small, reusable utility with clear behavior under repeated calls and invalid inputs.

Answer Strategy

Deep clone is the interview question that exposes how well you understand reference identity. The naive answer (JSON.parse(JSON.stringify(value))) silently corrupts Dates, RegExp, Map, Set, undefined, BigInt, functions, and cycles, and it loses the prototype chain. A senior solution states that contract before writing code.

The trick for cycles is a single sentence: register the new container in a WeakMap before recursing into its children, so any descendant that loops back resolves to the same fresh reference. The same WeakMap also collapses shared sub-trees, which preserves the original aliasing instead of duplicating identity.

Volunteer the failure modes. The browser ships structuredClone, which is the right answer in production for everything it supports. Hand-rolled cloning matters in interviews because you have to name what structuredClone does not handle (functions, DOM nodes, class instances with private state, getters/setters) and what your implementation deliberately preserves (prototype, cycles, Map/Set/Date/RegExp). Be explicit about the boundary instead of pretending to handle every value.

Reference Implementation: Cycle-Safe Deep Clone

A WeakMap-based recursive clone that handles plain objects, arrays, Dates, RegExps, Maps, Sets, class prototypes, and cycles. Functions and DOM nodes are intentionally shared by reference.

function deepClone<T>(value: T, seen: WeakMap<object, unknown> = new WeakMap()): T {
  // Primitive and function values share by reference; that is the documented
  // behavior of structuredClone too. Functions are not cloneable in general.
  if (value === null || typeof value !== 'object') return value;

  // The seen map is the entire trick for cyclic graphs. Before recursing into
  // children, register a placeholder for the *current* container so any
  // descendant that points back to it resolves to the same new reference.
  const cached = seen.get(value as object);
  if (cached !== undefined) return cached as T;

  if (value instanceof Date) {
    const copy = new Date(value.getTime());
    seen.set(value as object, copy);
    return copy as unknown as T;
  }

  if (value instanceof RegExp) {
    const copy = new RegExp(value.source, value.flags);
    copy.lastIndex = value.lastIndex;
    seen.set(value as object, copy);
    return copy as unknown as T;
  }

  if (value instanceof Map) {
    const copy = new Map();
    seen.set(value as object, copy);
    for (const [key, entry] of value as Map<unknown, unknown>) {
      copy.set(deepClone(key, seen), deepClone(entry, seen));
    }
    return copy as unknown as T;
  }

  if (value instanceof Set) {
    const copy = new Set();
    seen.set(value as object, copy);
    for (const entry of value as Set<unknown>) {
      copy.add(deepClone(entry, seen));
    }
    return copy as unknown as T;
  }

  if (Array.isArray(value)) {
    const copy: unknown[] = [];
    seen.set(value as object, copy);
    for (let index = 0; index < value.length; index = 1) {
      copy[index]= deepClone(value[index], seen);
    }
    return copy as unknown as T;
  }

  // Plain object path: preserve the prototype so class instances do not
  // collapse to bare objects mid-clone. This matches structuredClone for
  // objects with no transferable contents.
  const prototype= Object.getPrototypeOf(value);
  const copy= Object.create(prototype) as Record<PropertyKey, unknown>;
  seen.set(value as object, copy);

  for (const key of Reflect.ownKeys(value as object)) {
    const descriptor = Object.getOwnPropertyDescriptor(value as object, key);
    if (!descriptor) continue;
    if ('value' in descriptor) {
      Object.defineProperty(copy, key, {
        ...descriptor,
        value: deepClone(descriptor.value, seen),
      });
    } else {
      // Getters/setters cannot be deep-cloned safely. Preserve the descriptor
      // verbatim and document the behavior.
      Object.defineProperty(copy, key, descriptor);
    }
  }

  return copy as T;
}

Runnable Playground

Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.


function deepClone<T>(value: T, seen: WeakMap<object, unknown> = new WeakMap()): T {
  // Primitive and function values share by reference; that is the documented
  // behavior of structuredClone too. Functions are not cloneable in general.
  if (value === null || typeof value !== 'object') return value;

  // The seen map is the entire trick for cyclic graphs. Before recursing into
  // children, register a placeholder for the *current* container so any
  // descendant that points back to it resolves to the same new reference.
  const cached = seen.get(value as object);
  if (cached !== undefined) return cached as T;

  if (value instanceof Date) {
    const copy = new Date(value.getTime());
    seen.set(value as object, copy);
    return copy as unknown as T;
  }

  if (value instanceof RegExp) {
    const copy = new RegExp(value.source, value.flags);
    copy.lastIndex = value.lastIndex;
    seen.set(value as object, copy);
    return copy as unknown as T;
  }

  if (value instanceof Map) {
    const copy = new Map();
    seen.set(value as object, copy);
    for (const [key, entry] of value as Map<unknown, unknown>) {
      copy.set(deepClone(key, seen), deepClone(entry, seen));
    }
    return copy as unknown as T;
  }

  if (value instanceof Set) {
    const copy = new Set();
    seen.set(value as object, copy);
    for (const entry of value as Set<unknown>) {
      copy.add(deepClone(entry, seen));
    }
    return copy as unknown as T;
  }

  if (Array.isArray(value)) {
    const copy: unknown[] = [];
    seen.set(value as object, copy);
    for (let index = 0; index < value.length; index += 1) {
      copy[index] = deepClone(value[index], seen);
    }
    return copy as unknown as T;
  }

  // Plain object path: preserve the prototype so class instances do not
  // collapse to bare objects mid-clone. This matches structuredClone for
  // objects with no transferable contents.
  const prototype = Object.getPrototypeOf(value);
  const copy = Object.create(prototype) as Record<PropertyKey, unknown>;
  seen.set(value as object, copy);

  for (const key of Reflect.ownKeys(value as object)) {
    const descriptor = Object.getOwnPropertyDescriptor(value as object, key);
    if (!descriptor) continue;
    if ('value' in descriptor) {
      Object.defineProperty(copy, key, {
        ...descriptor,
        value: deepClone(descriptor.value, seen),
      });
    } else {
      // Getters/setters cannot be deep-cloned safely. Preserve the descriptor
      // verbatim and document the behavior.
      Object.defineProperty(copy, key, descriptor);
    }
  }

  return copy as T;
}
TypeScript · runnable

Testing Strategy

Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.

Identity
Assert the clone is structurally equal but reference-distinct at every nested level. Add a cyclic test that fails the naive JSON solution.
Built-ins
Cover Date, RegExp (with flags + lastIndex), Map, Set, and a class instance. Document what is intentionally not cloned (functions, DOM, getters/setters).
Performance
For trees of millions of nodes the recursion can blow the stack. Note when to switch to an iterative worklist or to structuredClone.
import { describe, it, expect } from 'vitest';

describe('deepClone', () => {
  it('clones plain nested objects without sharing references', () => {
    const source = { a: 1, b: { c: [1, 2, { d: 'deep' }] } };
    const clone = deepClone(source);
    expect(clone).toEqual(source);
    expect(clone).not.toBe(source);
    expect(clone.b).not.toBe(source.b);
    expect(clone.b.c[2]).not.toBe(source.b.c[2]);
  });

  it('preserves cycles', () => {
    const source: any = { name: 'root' };
    source.self = source;
    const clone = deepClone(source);
    expect(clone).not.toBe(source);
    expect(clone.self).toBe(clone);
    expect(clone.name).toBe('root');
  });

  it('clones Date, RegExp, Map, and Set', () => {
    const date = new Date('2026-04-01');
    const re = /foo/gi;
    re.lastIndex = 2;
    const map = new Map<string, number>([['a', 1]]);
    const set = new Set<number>([1, 2, 3]);

    const cloned = deepClone({ date, re, map, set });
    expect(cloned.date).not.toBe(date);
    expect(cloned.date.getTime()).toBe(date.getTime());
    expect(cloned.re.flags).toBe('gi');
    expect(cloned.re.lastIndex).toBe(2);
    expect(cloned.map.get('a')).toBe(1);
    expect(cloned.set.has(2)).toBe(true);
  });

  it('preserves class prototype', () => {
    class Person {
      constructor(public name: string) {}
      greet() {
        return 'Hi, ' + this.name;
      }
    }
    const ada = new Person('Ada');
    const clone = deepClone(ada);
    expect(clone).toBeInstanceOf(Person);
    expect(clone.greet()).toBe('Hi, Ada');
  });
});

Interviewer Signal

Tests whether you can turn a familiar utility into a precise contract instead of coding only the happy path.

Constraints

  • Define the function signature before coding.
  • Do not rely on global mutable state unless it is part of the returned closure.
  • Explain time and space cost for the common path.

Model Answer Shape

  • Write the smallest public contract first.
  • Cover empty input, repeated calls, thrown errors, and cleanup behavior.
  • Keep implementation readable enough to narrate under interview pressure.

Tradeoffs

  • A compact implementation is attractive, but explicit state names are easier to debug live.
  • Supporting every possible input can distract from the core contract; state the scope before coding.

Edge Cases

  • No arguments or undefined callbacks.
  • Synchronous throw inside the wrapped function.
  • Repeated calls before the previous result settles.

Testing And Proof

  • Happy path with representative inputs.
  • Boundary input and repeated invocation.
  • Cleanup or cancellation if timers or promises are involved.

Follow-Ups

  • How would you expose cancellation?
  • How would the API change for React usage?