javascriptreadonly-collection

JS equivalent to Java's Collections.unmodifiableCollection


I often use this strategy to my java code in order to make a Collection read only for the outside world, but avoid big/often clonings:

public abstract class MyClass {
    List<Long> myIds;

    public Collection<Long> getIds() {
        return Collections.unmodifiableCollection(this.myIds);
    }
}

I would like to follow the same pattern in my JS classes. This would make my business logic code much safer and cleaner, since I would be able to controll every change to my lists (push/splice etc) within the class that owns the fields.

At the moment use "private" fields for the lists and get-set functions for accessing them from outside. The only missing link is some equivalent to java's Collections.unmodifiableCollection. Something that would not copy the whole list (such as slice()), and that would not affect the original field (such as Object.freeze()).

Is there such a feature in JS? If not, how could someone achieve a similar effect (custom iterables?)


Solution

  • If you don't want to copy the object, and you want to make sure the original object can't be mutated externally, one option is to return a Proxy which throws when anything tries to mutate:

    const handler = {
      get(obj, prop) {
        return obj[prop];
      },
      set() {
        throw new Error('Setting not permitted');
      }
    }
    class MyClass {
      _myIds = ['foo', 'bar']
      getIds() {
        return new Proxy(this._myIds, handler);
      }
    }
    
    const instance = new MyClass();
    const ids = instance.getIds();
    console.log(ids);
    // Error:
    // ids[2] = 'baz';
    // Error:
    // ids.push('baz');

    Of course, instance._myIds is still technically visible - if you want to prevent that, you can use something like a WeakMap to ensure that the private array is truly only visible from inside the class.

    But Proxies are a bit slow. You might consider something like Typescript's ReadonlyArray instead - that way, you ensure that the code doesn't contain anything that mutates the array after it's returned, while keeping the actual code fast at runtime:

    private myIds = ['foo', 'bar']
    public getIds() {
      return this.myIds as ReadonlyArray<string>;
    }