JavaScript equivalent of Python’s setdefault for dictionaries

Python has setdefault which lets you set a default value for a dictionary entry.

There’s a good example here which shows making the default an array so you can do

dict[id].append(elem)

without having to check if dict[id] exists.

In JavaScript I could do this

const dict = {};
(dict[id] = dict[id] ?? []).push(elem)

But I was wondering if I could somehow implement similar functionality to python’s setdefault

I tried this

function makeObjectWithDefault(defaultFactory) {
  const obj = {};

  const handler = {
    get(target, prop, receiver) {
      let v = target[prop];
      if (v === undefined) {
        v = defaultFactory();
        target[prop] = v;
      }
      return v;
    },
  }

  return new Proxy(obj, handler)
}

It partly worked

const dict = makeObjectWithDefault(() => []);
dict['abc'].push(123);
dict.def.push(456);

But then unfortunately it adds extra properties

console.log(JSON.stringify(dict, null, 2));

prints

{
  "abc": [
    123
  ],
  "def": [
    456
  ],
  "toJSON": []     // !!! <- because it looked for a `toJSON` function
}

Because JSON.stringify looked for toJSON function on the object it ended up adding an array for that entry.

Of course I could check if the prop equals toJSON but that’s not really valid since I should be able to do dict['toJSON'].push(789) if I wanted to. And, I’d have to figure out which other functions try to access various properties (eg, toString)

Is it possible to implement python’s setdefault for JavaScript objects?

Alternatives:

  1. a helper

    Of course I could write a helper

    function accessWithDefaultArray(dict, id) {
      return (dict[id] = dict[id] ?? []);
    }
    
    accessWithDefaultArray(dict, 'abc').push(123);
    accessWithDefaultArray(dict, 'def').push(456);
    

    But it’s not very pretty

  2. I could write a class but it would require not using standard syntax

  3. A Map might work as a substitute since it uses get and set and so won’t
    suffer from confusion between access of methods and access of elements.
    In that case a simple class would work

    class MapWithDefault extends Map {
      constructor(defaultFactory, iterable) {
        super(iterable);
        this.defaultFactory = defaultFactory;
      }
      get(key) {
        let v = super.get(key);
        if (!v) {
          v = this.defaultFactory();
          super.set(key, v);
        }
        return v;
      }
    }
    
    {
      const d = new MapWithDefault(() => []);
      d.get('abc').push(123);
      d.get('def').push(456);
      d.get('def').push(789);
      console.log(JSON.stringify(Object.fromEntries(d.entries()), null, 2));
    }
    

    It’s not as nice as d['abc'].push(123) and d.def.push(456) and it’s not as directly
    usable as Object in various places.