Problem

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.

Examples

Example 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Input: 
obj = {"val": 10}, 
mutators = [
proxy => { proxy.val += 1; },
proxy => { proxy.val -= 1; }
]
Output: 
[
{"val": 11},
{"val": 9}
]
Explanation:
const helper = new ImmutableHelper({val: 10});
helper.produce(proxy => { proxy.val += 1; }); // { "val": 11 }
helper.produce(proxy => { proxy.val -= 1; }); // { "val": 9 }

Example 2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Input: 
obj = {"arr": [1, 2, 3]} 
mutators = [
proxy => { 
proxy.arr[0] = 5; 
proxy.newVal = proxy.arr[0] + proxy.arr[1];
}
]
Output: 
[
{"arr": [5, 2, 3], "newVal": 7 } 
]
Explanation: Two edits were made to the original array. The first element in the array was to set 5. Then a new key was added with a value of 7.

Example 3:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Input: 
obj = {"obj": {"val": {"x": 10, "y": 20}}}
mutators = [
proxy => { 
let data = proxy.obj.val; 
let temp = data.x; 
data.x = data.y; 
data.y = temp; 
}
]
Output: 
[
{"obj": {"val": {"x": 20, "y": 10}}}
]
Explanation: The values of "x" and "y" were swapped.

Constraints:

  • 2 <= JSON.stringify(obj).length <= 4 * 10^5
  • mutators is an array of functions
  • total calls to produce() < 10^5

Solution

Method 1 – Proxy-based Shallow Copy

Intuition

We want to allow users to “mutate” an object or array, but without changing the original. By using a Proxy, we can intercept mutations and apply them to a shallow copy, returning the new object while keeping the original unchanged.

Approach

  1. In the produce method, create a shallow copy of the original object (array or object).
  2. Use a Proxy to intercept set operations and apply them to the shallow copy.
  3. Pass the Proxy to the mutator function.
  4. Return the shallow copy after mutation.
  5. The original object remains unchanged.

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class ImmutableHelper {
  constructor(obj) {
    this.obj = obj;
  }
  produce(mutator) {
    const isArr = Array.isArray(this.obj);
    const ans = isArr ? [...this.obj] : { ...this.obj };
    const proxy = new Proxy(ans, {
      set(target, prop, value) {
        target[prop] = value;
        return true;
      },
      get(target, prop) {
        return target[prop];
      }
    });
    mutator(proxy);
    return ans;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class ImmutableHelper<T extends object> {
  obj: T;
  constructor(obj: T) {
    this.obj = obj;
  }
  produce(mutator: (proxy: T) => void): T {
    const isArr = Array.isArray(this.obj);
    const ans = isArr ? [...(this.obj as any)] : { ...this.obj };
    const proxy = new Proxy(ans, {
      set(target, prop, value) {
        target[prop] = value;
        return true;
      },
      get(target, prop) {
        return target[prop];
      }
    });
    mutator(proxy);
    return ans as T;
  }
}

Complexity

  • ⏰ Time complexity: O(n) — Shallow copy of the object/array is made, where n is the number of keys/elements.
  • 🧺 Space complexity: O(n) — Space for the shallow copy of the object/array.