Skip to main content
Back to problems
#2691
Hard JavaScript

Immutability helper

39.8% acceptance
Apr 1, 2026
11
8
Creating clones of immutable objects with minor alterations can be a tedious process. Write a class ImmutableHelper that serves as a tool to help with this requirement. The constructor accepts an immutable object obj which will be a JSON object or array. The class has a single method produce which accepts a function mutator. The function returns a new object which is similar to the original except it has those mutations applied. mutator accepts a proxied version of obj. A user of this function can (appear to) mutate this object, but the original object obj should not actually be effected. For example, a user could write code like this: const originalObj = {"x": 5}; const helper = new ImmutableHelper(originalObj); const newObj = helper.produce((proxy) => { proxy.x = proxy.x + 1; }); console.log(originalObj); // {"x": 5} console.log(newObj); // {"x": 6} Properties of the mutator function: It will always return undefined. It will never access keys that don't exist. It will never delete keys (delete obj.key) It will never call methods on a proxied object (push, shift, etc). It will never set keys to objects (proxy.x = {}) Note on how the solution will be tested: the solution validator will only analyze differences between what was returned and the original obj. Doing a full comparison would be too computationally expensive. Also, any mutations to the original object will result in a wrong answer.

Solution

TypeScript
Time O(n)
Space O(1)
LeetCode
solution.ts
type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
type InputObj = Record<string, JSONValue> | Array<JSONValue>;

class ImmutableHelper {
  obj: InputObj;
  constructor(obj: InputObj) { this.obj = obj; }

  produce(mutator: (obj: InputObj) => void): InputObj {
    // Node: base, sets (overrides), kids (child nodes), dirty, parent, proxy
    type N = { b: any; s: Map<string, any> | null; k: Map<string, N> | null; d: boolean; p: N | null; x: any };
    const root: N = { b: this.obj, s: null, k: null, d: false, p: null, x: null };

    // Create mutation-time proxy: tracks changes in Maps, no copying
    const px = (n: N): any => {
      if (n.x) return n.x;
      return n.x = new Proxy(n.b, {
        get(_, p) {
          if (typeof p === 'symbol') return n.b[p];
          if (n.s?.has(p)) return n.s.get(p);
          const v = n.b[p];
          if (v !== null && typeof v === 'object') {
            if (!n.k) n.k = new Map();
            let c = n.k.get(p);
            if (!c) { c = { b: v, s: null, k: null, d: false, p: n, x: null }; n.k.set(p, c); }
            return px(c);
          }
          return v;
        },
        set(_, p: string, val) {
          if (!n.s) n.s = new Map();
          n.s.set(p, val);
          let c: N | null = n;
          while (c && !c.d) { c.d = true; c = c.p; }
          return true;
        }
      });
    };

    mutator(px(root) as InputObj);
    if (!root.d) return this.obj;

    // Build result as Proxy overlay — O(mutations) instead of O(object_size)
    const build = (n: N): any => {
      if (!n.d) return n.b;
      const { s, b, k } = n;
      let cr: Map<string, any> | null = null;
      if (k) for (const [key, c] of k) if (c.d) (cr ??= new Map()).set(key, build(c));
      let ex: string[] | null = null;
      if (s) for (const key of s.keys()) if (!(key in b)) (ex ??= []).push(key);

      return new Proxy(b, {
        get(_, p) {
          if (typeof p === 'symbol') return b[p];
          if (s?.has(p)) return s.get(p);
          if (cr?.has(p)) return cr.get(p);
          return b[p];
        },
        has(_, p) {
          if (typeof p === 'string' && s?.has(p)) return true;
          return p in b;
        },
        ownKeys() {
          const keys = Reflect.ownKeys(b);
          if (ex) keys.push(...ex);
          return keys;
        },
        getOwnPropertyDescriptor(_, p) {
          if (typeof p === 'string') {
            if (s?.has(p)) return { value: s.get(p), writable: true, enumerable: true, configurable: true };
            if (cr?.has(p)) return { value: cr.get(p), writable: true, enumerable: true, configurable: true };
          }
          return Object.getOwnPropertyDescriptor(b, p);
        }
      });
    };

    return build(root) as InputObj;
  }
}

// --- Tests ---
function runTests() {
  // Example 1: simple increment/decrement
  const obj1 = {"val": 10};
  const helper1 = new ImmutableHelper(obj1);
  const r1a = helper1.produce(proxy => { (proxy as any).val += 1; });
  const r1b = helper1.produce(proxy => { (proxy as any).val -= 1; });
  console.assert(JSON.stringify(r1a) === '{"val":11}', "Test 1a failed");
  console.assert(JSON.stringify(r1b) === '{"val":9}', "Test 1b failed");
  console.assert(JSON.stringify(obj1) === '{"val":10}', "Test 1 original mutated");

  // Example 2: nested array + new key
  const obj2 = {"arr": [1, 2, 3]} as InputObj;
  const helper2 = new ImmutableHelper(obj2);
  const r2 = helper2.produce(proxy => {
    (proxy as any).arr[0] = 5;
    (proxy as any).newVal = (proxy as any).arr[0] + (proxy as any).arr[1];
  });
  console.assert(JSON.stringify(r2) === '{"arr":[5,2,3],"newVal":7}', "Test 2 failed");
  console.assert(JSON.stringify(obj2) === '{"arr":[1,2,3]}', "Test 2 original mutated");

  // Example 3: swap deep values
  const obj3 = {"obj": {"val": {"x": 10, "y": 20}}} as InputObj;
  const helper3 = new ImmutableHelper(obj3);
  const r3 = helper3.produce(proxy => {
    const data = (proxy as any).obj.val;
    const temp = data.x;
    data.x = data.y;
    data.y = temp;
  });
  console.assert(JSON.stringify(r3) === '{"obj":{"val":{"x":20,"y":10}}}', "Test 3 failed");
  console.assert(JSON.stringify(obj3) === '{"obj":{"val":{"x":10,"y":20}}}', "Test 3 original mutated");

  // Structural sharing: unchanged subtrees are same reference
  const obj4 = {"a": {"x": 1}, "b": {"y": 2}} as InputObj;
  const helper4 = new ImmutableHelper(obj4);
  const r4 = helper4.produce(proxy => { (proxy as any).a.x = 99; });
  console.assert((r4 as any).b === (obj4 as any).b, "Test 4 structural sharing failed");
  console.assert((r4 as any).a !== (obj4 as any).a, "Test 4 changed subtree should differ");

  // Max constraint: ~400KB object, many produce calls
  // Build large object: ~4 * 10^5 chars when stringified
  const bigObj: Record<string, JSONValue> = {};
  const numKeys = 5000;
  for (let i = 0; i < numKeys; i++) {
    bigObj[`key_${i.toString().padStart(5, '0')}`] = i;
  }
  const bigHelper = new ImmutableHelper(bigObj);
  const start = Date.now();
  // 10000 produce calls (scaled down from 10^5 for quick test, proportional)
  const totalCalls = 10000;
  let lastResult: any = null;
  for (let i = 0; i < totalCalls; i++) {
    lastResult = bigHelper.produce(proxy => {
      (proxy as any)[`key_${(i % numKeys).toString().padStart(5, '0')}`] += 1;
    });
  }
  const elapsed = Date.now() - start;
  // lastResult is from i=9999 which modifies key_04999 (9999 % 5000 = 4999)
  // Original value was 4999, incremented to 5000
  console.assert(lastResult[`key_04999`] === 5000, "Big test last mutation check failed");
  // key_00000 is NOT modified in the last call, so it keeps original value 0
  console.assert(lastResult[`key_00000`] === 0, "Big test unmodified key check failed");
  // Original should be untouched
  console.assert(bigObj[`key_00000`] === 0, "Big test original mutated");
  console.assert(bigObj[`key_04999`] === 4999, "Big test original key mutated");
  console.log(`Max constraint test: ${totalCalls} produce() calls on ${numKeys}-key obj in ${elapsed}ms`);

  // Deeply nested object stress test
  let deepObj: Record<string, JSONValue> = { "val": 0 };
  for (let i = 0; i < 50; i++) {
    deepObj = { "child": deepObj };
  }
  const deepHelper = new ImmutableHelper(deepObj);
  const deepResult = deepHelper.produce(proxy => {
    let cur: any = proxy;
    for (let i = 0; i < 50; i++) cur = cur.child;
    cur.val = 42;
  });
  // Verify mutation applied
  let cur: any = deepResult;
  for (let i = 0; i < 50; i++) cur = cur.child;
  console.assert(cur.val === 42, "Deep test mutation failed");
  // Verify original untouched
  let origCur: any = deepObj;
  for (let i = 0; i < 50; i++) origCur = origCur.child;
  console.assert(origCur.val === 0, "Deep test original mutated");

  console.log("All tests passed!");
}
runTests();