Update nested JS objects without overwriting missing properties

Viewed 848

EDIT: Thanks 4 all the great, diverse answers - I chose the solution that worked for me even after I realized that I needed more requirements: I also needed new properties to be added and for it to work with arrays in objects as well.

Here's what I wanna do: Update one object through another one.
Here are some constraints:

  • Only properties that the new object has should be updated (and that the original object also has), the other properties should remain unchanged, NOT DELETED
  • Nested objects should also be updated in the same way

My Problem is that I don't know how to easily do this for nested objects because typeof x === "object" also returns true for Date objects for example.

Here's what I got so far:

let originalObj = {
  dateCreated: new Date(2021, 1, 10),
  value: "100",
  subObj: {
    count: 55,
    val: null
  }
};

let updateObj = {
  dateCreated: new Date(2021, 1, 11),
  subObj: {
    val: 90
  }
};

let updateOrignal = (oObj, uObj) => {
  for (let prop in uObj) {
    if (uObj.hasOwnProperty(prop) &&
      oObj.hasOwnProperty(prop)) {
      oObj[prop] = uObj[prop];
    }
  }
};

console.log(originalObj);
updateOrignal(originalObj, updateObj)
console.log(originalObj);

Currently my updated object looks like this:

{
  "dateCreated": "2021-02-10T23:00:00.000Z",
  "value": "100",
  "subObj": {
    "val": 90
  }
}

My goal:

{
  "dateCreated": "2021-02-10T23:00:00.000Z",
  "value": "100",
  "subObj": {
    "count": 55,
    "val": 90
  }
}
4 Answers

This is the solution you need (from https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6), actually you can search on google "deep merge" or "recursively merge two javascript objects"

I added ES6 sintax {...obj} to be sure to clone objects before merging them

let originalObj = {
  dateCreated: new Date(2021, 1, 10),
  value: "100",
  subObj: {
    count: 55,
    val: null
  }
};

let updateObj = {
  dateCreated: new Date(2021, 1, 11),
  subObj: {
    val: 90
  }
};

const merge = (target, source) => {
  // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
  for (const key of Object.keys(source)) {
    if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
  }
  // Join `target` and modified `source`
  Object.assign(target || {}, source)
  return target
}

console.log(merge({ ...originalObj}, {...updateObj}));

You could test the constructor of the values like this:

let originalObj = {
  dateCreated: new Date(2021, 1, 10),
  value: "100",
  subObj: {
    count: 55,
    val: null
  }
};

let updateObj = {
  dateCreated: new Date(2021, 1, 11),
  subObj: {
    val: 90
  }
};

let updateOriginal = (original, patch) => {
  Object.entries(patch).forEach(([key, value]) => {
    value && value.constructor === Object && patch[key]
      ? updateOriginal(original[key], patch[key])
      : (original[key] = patch[key]);
  });
}

updateOriginal(originalObj, updateObj);

console.log(originalObj);

You could use recursive approach with reduce method and for..in loop and also check if the type of object is Date. This solution will not work with arrays and also will not modify original data.

let originalObj = {
  dateCreated: new Date(2021, 1, 10),
  value: "100",
  subObj: {
    count: 55,
    val: null
  }
};

let updateObj = {
  dateCreated: new Date(2021, 1, 11),
  subObj: {
    val: 90
  }
};

function update(o1, o2) {
  return Object.entries(o1).reduce((r, e) => {
    for (let p in o2) {
      if ([o1[p], o2[p]].every(o => typeof o === 'object')) {
        r[p] = o2[p] instanceof Date ? o2[p] : update(r[p], o2[p])
      } else {
        r[p] = o2[p]
      }
    }

    return r
  }, { ...o1 })
}

const result = update(originalObj, updateObj)

console.log(result)

This is what worked for me in the past. You can clean it up a little and customize for your purposes but the idea is as follows:

let originalObj = {
  dateCreated: new Date(2021, 1, 10),
  value: "100",
  subObj: {
    count: 55,
    val: null
  }
};

let updateObj = {
  dateCreated: new Date(2021, 1, 11),
  subObj: {
    val: 90
  }
};

function mergeDeep(target, source) {
  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) {
          Object.assign(target, { [key]: {} });
        }
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }
}

function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

console.log(originalObj);
mergeDeep(originalObj, updateObj);
console.log(originalObj);

Related