Cold_Class
Cold_Class

Reputation: 3484

Update nested JS objects without overwriting missing properties

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:

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
  }
}

Upvotes: 5

Views: 2365

Answers (4)

Nenad Vracar
Nenad Vracar

Reputation: 122057

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)

Upvotes: 2

farhodius
farhodius

Reputation: 530

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);

Upvotes: 1

Francesco Orsi
Francesco Orsi

Reputation: 2089

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}));

Upvotes: 4

Guerric P
Guerric P

Reputation: 31815

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);

Upvotes: 2

Related Questions