Search code examples
javascriptarrayses6-proxy

JavaScript Proxy which returns an empty array by default


I want to define some object so that every property provides an empty array if it's not defined.

Example: consider the list of users, every user is {id: number, age: string, ...}. We want to group users by age:

const users = [{id: 1, age: 42}, {id: 2, age: 42}, {id: 3, age: 43}]

const usersByAge = users.reduce((out, user) => {
  if (!out.hasOwnProperty(user.age)) {
    out[user.age] = []
  }
  out[user.age].push(user)
  return out
}, {})

console.log(usersByAge[42]) -> [{id: 1, age: 42}, {id: 2, age: 42}]
console.log(usersByAge[43]) -> [{id: 3, age: 43}]
console.log(usersByAge[18]) -> undefined
console.log(Object.keys(usersByAge)) -> ['42', '43']

This code works, but I want to shorten it by using special JavaScript Proxy. It should return an empty array if the property is not defined. It should be possible to push items into the property even if it's not defined. It should provide the same array for different undefined properties:

const getMapFromIdToArray = () => {
  return new Proxy({}, {
    get: (target, name) => {
      if (!target.hasOwnProperty(name)) {
        target[name] = []
      }
      return target[name]
    }
  })
}

const usersByAge = users.reduce((out, user) => {
  out[user.age].push(user) // <- this should work, array initialization should not be required
  return out
}, getMapFromIdToArray())

console.log(usersByAge[42]) -> [{id: 1, age: 42}, {id: 2, age: 42}] - OK
console.log(usersByAge[18]) -> [] - OK
console.log(usersByAge[18] === usersByAge[19]) -> true - FAIL
console.log(Object.keys(usersByAge)) -> ['42', '43'] - FAIL

This code works partly. If the property is not defined, it's initialized as []. So the getter actually mutates the target. I would like to avoid this. But the same time I want to be able to call .push on an undefined property without initialization. Is it possible to achieve?


Solution

  • It should provide the same array for different undefined properties

    I'm not sure what you meant in this sentence, but you definitely don't want two different properties to return reference to the same array and then items belonging to different properties being pushed to that array.

    If the property is not defined, it's initialized as []. So the getter actually mutates the target. I would like to avoid this. But the same time I want to be able to call .push on an undefined property without initialization. Is it possible to achieve?

    I would say yes and no. The push method has to work on an actual array, and the object must hold a reference to that array in order to retrive the pushed items later. But if you don't want the empty array to create an actual property in the target object right away, then you can store it separately in the proxy until it requested after being mutated:

    new Proxy({}, {
      tempProps: {},
      get: (target, name, reciever) => {
        if (target.hasOwnProperty(name)) {
          return target[name];
        }
    
        if (tempProps.hasOwnProperty(name)) {
          if tempProps[name].length {
            // array is not empty - bring into target
            target[name] = tempProps[name];
            delete tempProps[name];
            return target[name];
          } else {
            return tempProps[name];
          }
        }
    
        tempProps[name] = [];
        return tempProps[name];
      }
    })
    

    It may also be possible to hold the reference to the returned empty array in a WeakRef object, so that if the caller drops all references to the array it will finally be garbage collected. But that seems unnecessarily complicated unless in very specific circumstances.