Skip to content

2692. Make Object Immutable 👍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
type RecursiveHandler = {
  set: <T extends object>(target: T, prop: string, value: any) => boolean;
  get: <T extends object>(target: T, prop: string) => any;
  apply: <T extends Function>(target: T, thisArg: any, argArray?: any) => any;
};

function makeImmutable<T extends object | Function>(obj: T): T {
  // a set of mutating array methods
  const methods = new Set([
    'pop',
    'push',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse',
  ]);

  const handler: RecursiveHandler = {
    // 'set' trap prevents modifications of the object properties.
    set(target, prop, _) {
      throw Array.isArray(target)
        ? `Error Modifying Index: ${String(prop)}`
        : `Error Modifying: ${String(prop)}`;
    },
    // 'get' trap returns object properties or creates new proxies for nested
    // objects or functions
    get(target, prop) {
      // If the property is 'prototype', null, or not an object/function, return
      // it directly. We exclude 'prototype' to avoid potential issues with
      // inheritance Objects and functions are wrapped in a new proxy to
      // preserve immutability at all depths.
      const key = prop as keyof typeof target;
      return prop === 'prototype' ||
        target[key] === null ||
        (typeof target[key] !== 'object' && typeof target[key] !== 'function')
        ? target[key]
        : new Proxy(target[key], this);
    },
    // 'apply' trap prevents call of mutating methods and apply function calls.
    apply(target, thisArg, argumentsList) {
      if (methods.has((target as any).name))
        throw `Error Calling Method: ${(target as any).name}`;
      return target.apply(thisArg, argumentsList);
    },
  };

  return new Proxy(obj, handler) as T;
}