8

I am trying to deeply assign a value in an object. For example:

const errors = {}
if(errorOnSpecificField) {
  // TypeError: Cannot read property 'subSubCategory' of undefined(…)
  errors.subCategory.subSubCategory.fieldWithError = 'Error Message'
}

Right now, without lodash, I can do:

const errors = {}
if(errorOnSpecificField) {
    errors.subCategory = errors.SubCategory || {}
    errors.subCategory.subSubCategory = errors.SubCategory.subSubCategory || {}
    errors.subCategory.subSubCategory.fieldWithError = 'Error Message'
}

With lodash, I can do this:

const errors = {}
if(errorOnSpecificField) {
    _.set(errors, 'subCategory.subSubCategory.fieldWithError', 'Error Message');
}

I am trying to avoid using a third party library. Is there a more elegant solution, especially now that es2015 has object destructuring. The inverse operation is easy:

  let {subCategory : {subSubCategory: {fieldWithError}}} = errors

What is an elegant solution to deep object assignment? Thanks!

aherriot
  • 4,495
  • 6
  • 24
  • 36

4 Answers4

0

Here's a fairly readable way to safely assign to the deep object:

(((errors||{}).subCategory||{}).subSubCategory||{}).fieldWithError = 'Error Message'

That doesn't create errors.subCategory.subSubCategory if it doesn't already exist, though.

Bo Borgerson
  • 1,386
  • 10
  • 20
  • 2
    This actually doesn't work for assignment. For accessing, yes, but not for creating properties. – Bergi Jun 15 '16 at 19:34
  • Hmm... It won't create `errors.subCategory.subSubCategory`, but it will safely assign to it if it already exists. – Bo Borgerson Jun 15 '16 at 19:39
0

Short answer, no there is no clean way of doing this without writing a method for it (tbh you could just use the method from lodash without importing the whole library)

... however ...

WARNING This is for fun only. Do not try this in production (req es6).

Object.prototype.chainSet = function() {
  let handler = {
    get (target, name) {
      if (!(name in target)) {
        target[name] = new Proxy({}, handler)
      }
      return target[name]
    }
  }

  return new Proxy(this, handler)
}

use:

let a = {}
a.chainSet().foo.bar.baz = 1
a.foo.bar.baz // => 1
Damon
  • 4,216
  • 2
  • 17
  • 27
0

Object.assign() will work just fine for what you're asking.

let errors = { otherField: "value" };
let newobj = {subCategory : {subSubCategory: {fieldWithError: "Error goes here"}}};
Object.assign(errors, newobj);

This yields:

{
  otherField:'value',
  subCategory: {
    subSubCategory: {
      fieldWithError:'Error goes here'
    }
  }
}
Cobus Kruger
  • 8,338
  • 3
  • 61
  • 106
  • 1
    if `errors = {subCategory: {x: "y"}}` and you assign `newobj`, then property `x` gets "removed" (the whole `subCategory` object is swapped out). OP is looking for a way to avoid that. – le_m Jun 15 '16 at 21:20
0

You could try something like below:

function ErrorRegistry(obj)
{
    this.errors = obj || {};
    this.addError = function(k, msg)
    {
        var keys = k.split('.');
        var o = this.errors;
        for(var i = 0, l = keys.length, last = l-1; i<l; i++)
        {
            if(typeof o[keys[i]] === 'undefined')
                o[keys[i]] = {};
            if(i == last)
                o[keys[i]] = msg;
            else
                o = o[keys[i]];
        }
    };
}
var errors = {'subCategory1':{'fieldWithError1':'Error1'}};
var errorRegistry = new ErrorRegistry(errors);
errorRegistry.addError('subCategory1.fieldWithError2', "Error2");
errorRegistry.addError('subCategory1.subSubCategory1.fieldWithError3', "Error3");
errorRegistry.addError('subCategory1.subSubCategory2.fieldWithError4', "Error4");
errors = errorRegistry.errors;
console.log(errors);
Thalaivar
  • 23,282
  • 5
  • 60
  • 71