user2161301
user2161301

Reputation: 834

JavaScript: Write to dictionary if key doesn't exist

I want to populate a dictionary of dictionaries in JavaScript.

As soon as I have a resident's address and name, I want to write it into the object, i.e.

addressCache[city][street][number] = name

However, if my dictionary doesn't contain a value for the city's key yet, it's accessing a street of undefined since the dictionary does not exist yet.

My workaround at the moment is

adressCache = {}
function setAddressEntry(city, street, number, familyName) {
  if(!adressCache[city]) {
    adressCache[city] = {}
  }
  if(!adressCache[city][street]) {
    adressCache[city][street] = {}
  }

  adressCache[city][street][number] = familyName
  
}

I don't think that's the right way of doing that. How can I improve this code? Or is it common to write a helper function for that?

Upvotes: 3

Views: 4709

Answers (3)

VLAZ
VLAZ

Reputation: 29087

Using a proxy

This can be generalised to any object by using a Proxy:

const infiniteChainHandler = {
  get(target, prop, receiver) {
    if (!(prop in target))
        target[prop] = {};
    
    const result = target[prop];
    
    if (typeof result === "object" && result !== null) {
      return new Proxy(result, infiniteChainHandler);
    }
    
    return result;
  }
};

/* ... */

new Proxy(adressCache, infiniteChainHandler)[city1][street1][number1] = familyName1;

Demo:

const infiniteChainHandler = {
  get(target, prop, receiver) {
    if (!(prop in target))
        target[prop] = {};
    
    const result = target[prop];
    
    if (typeof result === "object" && result !== null) {
      return new Proxy(result, infiniteChainHandler);
    }
    
    return result;
  }
};


/* Example usage: */


const adressCache = {};

const city1 = "Oz";
const street1 = "Yellow Brick Road";
const number1 = "1";

const familyName1 = "Gale";
new Proxy(adressCache, infiniteChainHandler)[city1][street1][number1] = familyName1;


const street2 = "Wizard boulevard";
const number2 = "12";

const familyName2 = "The Wizard";
new Proxy(adressCache, infiniteChainHandler)[city1][street2][number2] = familyName2;


new Proxy(adressCache, infiniteChainHandler)["something"]["else"] = "here";
new Proxy(adressCache, infiniteChainHandler)
  .any
  .very
  .long
  .and
  .nested
  .path
  .is
  .also
  .supported
  .using
  .this = "approach";


console.log(adressCache);

  1. Wrap the object in the proxy.
  2. Use the get trap and when reading any property, create it if it's not there and set it to an empty object.
  3. If the returned value would be an object, wrap it in a Proxy object again using the same handler.
  4. If the return value is anything else, just return it.

This allows you to chain any amount of properties and have the path automatically created.

Note that the above wraps addressCache into the proxy every time to keep the variable itself a simple object. If you start with const adressCache = new Proxy(adressCache, infiniteChainHandler) it makes extracting the data out harder.

To avoid having to write new Proxy all the time, you can use something like this for convenience:

/* library code */
const wrap = handler => obj =>
  new Proxy(obj, handler);
/* /library code */

/* ... */

const chainable = wrap(infiniteChainHandler);

/* ... */

chainable(adressCache)[city1][street1][number1] = familyName1;

/* library code */
const wrap = handler => obj =>
  new Proxy(obj, handler);
/* /library code */

const infiniteChainHandler = {
  get(target, prop, receiver) {
    if (!(prop in target))
        target[prop] = {};
    
    const result = target[prop];
    
    if (typeof result === "object" && result !== null) {
      return new Proxy(result, infiniteChainHandler);
    }
    
    return result;
  }
};

const chainable = wrap(infiniteChainHandler);


/* Example usage: */


const adressCache = {};

const city1 = "Oz";
const street1 = "Yellow Brick Road";
const number1 = "1";

const familyName1 = "Gale";
chainable(adressCache)[city1][street1][number1] = familyName1;


const street2 = "Wizard boulevard";
const number2 = "12";

const familyName2 = "The Wizard";
chainable(adressCache)[city1][street2][number2] = familyName2;


chainable(adressCache)["something"]["else"] = "here";
chainable(adressCache)
  .any
  .very
  .long
  .and
  .nested
  .path
  .is
  .also
  .supported
  .using
  .this = "approach";


console.log(adressCache);

Use a function

This instead generalises the function that does the assignment itself to work with any object and any path length:

const assignTo = obj => (...path) => value => {
  const lastProp = path.pop();
  
  let target = obj;
  for (const prop of path) {
    if (!(prop in target))
      target[prop] = {};
    
    target = target[prop];
  }
  
  target[lastProp] = value;
}

/* ... */

const adressCache = {};
const addAdress = assignTo(adressCache);

/* ... */

addAdress (city1, street1, number1) (familyName1);

/* library code */
const assignTo = obj => (...path) => value => {
  const lastProp = path.pop();
  
  let target = obj;
  for (const prop of path) {
    if (!(prop in target))
      target[prop] = {};
    
    target = target[prop];
  }
  
  target[lastProp] = value;
}
/* /library code */


/* Example usage: */


const adressCache = {};
const addAdress = assignTo(adressCache);

const city1 = "Oz";
const street1 = "Yellow Brick Road";
const number1 = "1";

const familyName1 = "Gale";
addAdress (city1, street1, number1) (familyName1);


const street2 = "Wizard boulevard";
const number2 = "12";

const familyName2 = "The Wizard";
addAdress (city1, street2, number2) (familyName2);


addAdress ("something", "else") ("here");
addAdress (
  "any",
  "very",
  "long",
  "and",
  "nested",
  "path",
  "is",
  "also",
  "supported",
  "using",
  "this") ("approach");


console.log(adressCache);

  1. Separate out the last property.
  2. Go through the entire path and assign an object if it's not there.
  3. Assign the value to the last property of the path provided.

This solution uses multiple arrow functions (with variable amount of arguments at the second step) for a bit of simplicity. You could also take the entire path and the value as parameters but then the calls will be uglier. This makes it clearer what is the path, what is the value.

Upvotes: 2

Punith Mithra
Punith Mithra

Reputation: 628

I don't see any problem with your code. That is how you set a value for an object which don't have a proper path to a specific property.

some code improvisations:

adressCache = {}
function setAddressEntry(city, street, number, familyName) {
    adressCache[city] = adressCache[city] || {};
    adressCache[city][street] = adressCache[city][street] || {};
    adressCache[city][street][number] = familyName;
}

Upvotes: 2

DecPK
DecPK

Reputation: 25406

It would be much easier and clean with Logical nullish assignment (??=)

let adressCache = {};
function setAddressEntry(city, street, number, familyName) {
  adressCache.city ??= {};
  adressCache.city.street ??= {};

  adressCache.city.street.number = familyName;
}

setAddressEntry("gurugram", "1st street", 24, "SULTAN");
console.log(adressCache);

Upvotes: 6

Related Questions