0

I've been trying to initialize an object in case of missing properties, in a similar way we would do for a variable by using the OR operator:

var finalValue = myVariable || 'hello there'

I've found plenty replies on how to do this by using the Object.assign() method:

finalObject = Object.assign({}, defaultObject, myObject)

Still, I didn't manage to find a proper solution when it comes to an object with nested data. Here's what I came up to:

var arrayConstructor = [].constructor;
var objectConstructor = ({}).constructor;

var whatIsIt = function(object) {
    if (object === null) {
        return "null";
    } else
    if (object === undefined) {
        return "undefined";
    } else
    if (object.constructor === arrayConstructor) {
        return "array";
    } else
    if (object.constructor === objectConstructor) {
        return "object";
    } else {
        return "normal variable";
    }
}

var copyWithValidation = function(jsonObject, defaultValues) {
    let defaultKeys = Object.keys(defaultValues);
    let returnValue = Object.assign({}, defaultValues, jsonObject);

    // Check for json object
    defaultKeys.map((key) => {

        switch (whatIsIt(defaultValues[key])) {
            case 'object':
                if (!jsonObject.hasOwnProperty(key)) {
                    jsonObject[key] = {};
                }
                returnValue[key] = copyWithValidation(jsonObject[key], defaultValues[key]);
                break;
        
            case 'array':
                if (jsonObject.hasOwnProperty(key)) {
                    returnValue[key] = [...jsonObject[key]];
                } else {
                    returnValue[key] = [...defaultValues[key]];
                }
                break;
        
            default:
                break;
        }
    });
    return returnValue;
}

// My default object
var def1 = {
    label1: '',
    label2: '',
    label3: '',
    sublabels: {
        label1: '',
        label2: '',
        label3: '',
    },
    sublabels2: {
        label1: '',
        label2: '',
        label3: '',
        label4: {
            title: '',
            subtitle: '',
        },
    },
    sublabels3: {
        label1: [],
        label2: [],
        label3: [],
    },
}

// The object we want to initialize
var j1 = {
    label1: 'hello1',
    label2: 'hello2',
    sublabels: {
        label2: 'subhello2',
        label3: 'subhello3',
    },
    sublabels3: {
        label1: ['subhello1', 'subhello2', 'subhello3'],
        label4: {
            title: 'Hello there!',
            subtitle: 'Hello there again',
        },
    },
}

// Let's run the script!
let p = copyWithValidation(j1, def1);
console.log(p);

The whole idea is to traverse in the object and initially using the "Object.assign({}, defaultValues, jsonObject)" method to "copy" the attributes for the specific level. After that, I'm checking if there are objects or arrays.

In case of an object, I'm setting {} to the target object (in case the property does not exist) and calling the same copyWithValidation() method inside that object; recursively it will cover all levels.

In case of an array I'm simply using the spread operator. The interesting thing is that this works even for objects as array elements.

Not sure though if there's a more efficient code which would return the same result.

Kudos to Aamir Afridi and Joaquin Keller for using/combining their scripts.

  • if you are willing to use a library, lodash's [merge](https://lodash.com/docs/4.17.15#merge) does exactly what you want – thedude Jul 10 '20 at 19:27
  • It would probably still be more efficient to copy each key only if it exists within `.map` rather than using Object.assign, since it avoids recopying duplicate keys, and at least under V8 Object.assign is still slower than just using Object spread, which would still be slower than only copying properties that you need and it'd be one line after `default:`. It isn't any more concise either, since you need the loop and iterating on keys no matter what. Also, you aren't using the return value of `.map`, so you should be using `.forEach` or a for-of loop – user120242 Jul 11 '20 at 02:23

1 Answers1

0

You can target the nesting with recursion. To note, there exists a discrepancy in the fact that the template does not contain a sublabels3.label4 property, while the data object does. The code is configured to add missing properties if this occurs.

var def1 = {
    label1: '',
    label2: '',
    label3: '',
    sublabels: {
        label1: '',
        label2: '',
        label3: '',
    },
    sublabels2: {
        label1: '',
        label2: '',
        label3: '',
        label4: {
            title: '',
            subtitle: '',
        },
    },
    sublabels3: {
        label1: [],
        label2: [],
        label3: []   
    },
}

// The object we want to initialize
var j1 = {
    label1: 'hello1',
    label2: 'hello2',
    sublabels: {
        label2: 'subhello2',
        label3: 'subhello3',
    },
    sublabels3: {
        label1: ['subhello1', 'subhello2', 'subhello3'],
        label4: {
            title: 'Hello there!',
            subtitle: 'Hello there again',
        },
    },
}

function initializer(data,template) {
    return Object.entries(data).reduce((initialized, [key, value]) =>{
        if(typeof value === "object" && !Array.isArray(value)){
            initialized[key] = initializer(value, initialized[key] || {}) 
            return initialized
        } else {
            initialized[key] = value;
            return initialized
        }      
    },template)
}

console.log(initializer(j1,def1))
Pavlos Karalis
  • 2,893
  • 1
  • 6
  • 14