80

I have an Object a like that:

const a = {
  user: {
   …
   groups: […]
   …
  }
}

whereby there are a lot more properties in a.user

And I would like to change only the a.user.groups value. If I do this:

const b = Object.assign({}, a, {
  user: {
    groups: {}
  }
});

b doesn't have any other Property except b.user.groups, all others are deleted. Is there any ES6 way to only change the nested property, without loosing all the other, with Object.assign?

philipp
  • 15,947
  • 15
  • 61
  • 106
  • 7
    imo it looks to be doing what `Object.assign` is supposed to. It's changing the `user` property with the new object you provided. If it's the only thing you want to change why don't you simply do `b.user.groups = /* value */`? – taguenizy Jan 11 '17 at 10:24
  • You might be interested by the [assign-deep](https://www.npmjs.com/package/assign-deep) or the [object-assign-deep](https://www.npmjs.com/package/object-assign-deep) packages. – Gras Double Jan 14 '23 at 04:38
  • Refs this question about the same issue: https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge – Gras Double Jan 14 '23 at 04:57

10 Answers10

145

After some trying I could find a solution that looks pretty nice like that:

const b = Object.assign({}, a, {
  user: {
    ...a.user,
    groups: 'some changed value'
  }
});

To make that answer more complete here a tiny note:

const b = Object.assign({}, a)

is essentially the same as:

const b = { ...a }

since it just copies all the properties of a (...a) to a new Object. So the above can written as:

 const b = {
   ...a,          //copy everything from a
   user: {        //override the user property
      ...a.user,  //same sane: copy the everything from a.user
      groups: 'some changes value'  //override a.user.group
   }
 }
philipp
  • 15,947
  • 15
  • 61
  • 106
  • 6
    I upvoted because of explaining well and using syntaxic sugar :) Just passing by, if you want to return an array with a new element inside, it's basically the same mechanism : [ ...someArray, newElement ]. – Alex Jul 18 '18 at 13:56
  • I must say, exactly what I was looking for and it did the job perfectly. – Shahrukh Anwar Apr 01 '20 at 08:02
  • Object.assign and spread operator are not exactly the same. Spread operator doesn't copy the prototype while Object.assign does – jonybekov Oct 13 '21 at 14:33
16

Here's a small function called Object_assign (just replace the . with a _ if you need nested assigning)

The function sets all target values by either pasting the source value in there directly, or by recursively calling Object_assign again when both the target value and source value are non-null objects.

const target = {
  a: { x: 0 },
  b: { y: { m: 0, n: 1      } },
  c: { z: { i: 0, j: 1      } },
  d: null
}

const source1 = {
  a: {},
  b: { y: {       n: 0      } },
  e: null
}

const source2 = {
  c: { z: {            k: 2 } },
  d: {}
}

function Object_assign (target, ...sources) {
  sources.forEach(source => {
    Object.keys(source).forEach(key => {
      const s_val = source[key]
      const t_val = target[key]
      target[key] = t_val && s_val && typeof t_val === 'object' && typeof s_val === 'object'
                  ? Object_assign(t_val, s_val)
                  : s_val
    })
  })
  return target
}

console.log(Object_assign(Object.create(target), source1, source2))
Gust van de Wal
  • 5,211
  • 1
  • 24
  • 48
  • It works like a charm. my case was the following : I have set up a custom property on a nested object of my root one. This property only exists on the front-side for a graphical enhancement purpose. Then, when I execute an action from my root object that call the back-end I used Object.assign to refresh my root object with data sent by the server. Unfortunately my custom property on the nested object was 'deleted'. Thanks to your function it's not the case anymore. – stephane brun Feb 18 '22 at 15:33
  • hmm I found a use case that totally mess your algorithm : my backend handles a list where items are sorted by 'creationDate desc'. So when from my front-end end I ask the back-end to add a new element into my list, when the server sends me back data, the item previously indexed at position '0' is now indexed at position '1' (and so on...). Due to this index shifting, your algorithm corrupts all my data model :( – stephane brun Feb 24 '22 at 14:50
  • @stephanebrun Oof. You shouldn't ever assume objects fields keep their order. Yes, their order is _deterministic_, but there is no "clean/minimal" way of guaranteeing it. If you need to have the values be logically indexed, you should have the keys be their index. Other options would be using arrays or maps. The fastest (not so clean) way I can think of right now, would be to cache all the keys as they come, then create the resulting object according to that order again. – Gust van de Wal Feb 25 '22 at 11:26
  • yes I think indexing objects with their keys will solve the problem. Anyway your algorithm is great. I keep it. It's just I have a weird case and I'm trying to find a solution that work for all the cases. But I think it is just (easily) impossible. Thanks for your time! – stephane brun Feb 25 '22 at 11:40
8

You asked specifically for an ES6 way with Object.assign, but maybe someone else will prefer my more 'general' answer - you can do it easily with lodash, and personally I think that this solution is more readable.

import * as _ from 'lodash';
_.set(a, 'user.groups', newGroupsValue);

It mutates object.

Patryk Wlaź
  • 493
  • 5
  • 11
  • 1
    Well of course, that's why I assumed that you would not like to use my solution but someone else with similar problem might find it useful. – Patryk Wlaź May 19 '17 at 11:02
5

You can change it this way,

const b = Object.assign({}, a, {
  user: Object.assign({}, a.user, {
          groups: {}
        })
});
pratZ
  • 3,078
  • 2
  • 20
  • 29
4

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

assign method will make a new object by copying from the source object, the issue if you change any method later on in the source object it will not be reflected to the new object.

Use create()

The Object.create() method creates a new object with the specified prototype object and properties.

const b = Object.create(a)
b.user.groups = {}
// if you don't want the prototype link add this
// b.prototype = Object.prototype 

This way you have b linked to a via the prototype and if you make any changes in a it will be reflected in b, and any changes in b will not affect a

Abdullah Alsigar
  • 1,236
  • 1
  • 12
  • 16
4

More general solution below. It makes recursively assigning only if there are conflicts (same key names). It resolves issues assigning complex objects, which has many reference points. For example, nested-object-assign, an existing package for nested object assign in npm, stuck with complex objects assign because it recursively goes through all keys.

var objecttarget = {
  a: 'somestring',
  b: {x:2,y:{k:1,l:1},z:{j:3,l:4}},
  c: false,
  targetonly: true
};

var objectsource = {
  a: 'somestring else',
  b: {k:1,x:2,y:{k:2,p:1},z:{}},
  c: true,
  sourceonly: true
};

function nestedassign(target, source) {
  Object.keys(source).forEach(sourcekey=>{
    if (Object.keys(source).find(targetkey=>targetkey===sourcekey) !== undefined && typeof source[sourcekey] === "object") {
      target[sourcekey]=nestedassign(target[sourcekey],source[sourcekey]);
    } else {
      target[sourcekey]=source[sourcekey];
    }
  });
  return target;
}

// It will lose objecttarget.b.y.l and objecttarget.b.z
console.log(Object.assign(Object.create(objecttarget),objectsource));

// Improved object assign
console.log(nestedassign(Object.create(objecttarget),objectsource));
Won
  • 1,795
  • 2
  • 12
  • 22
  • Good one, only this line: `if (Object.keys(` **source** `).find( targetkey =>...` should start with `if (Object.keys(` **target** `).find( targetkey =>` – sicko Aug 06 '21 at 08:53
  • @sicko Thanks for the feedback but you're wrong. If you change like that, you'll lose values that are only existed in source. Result is based on target. That's why iteration is based on source to add them to target even they are not existed in target. I added objecttarget.targetonly and objectsource.sourceonly in snippet. Try change code like you pointed. You'll see that result doesn't have sourceonly key and value. – Won Oct 20 '21 at 21:50
  • Please see my answer for the correct NestedObjectAssign(): https://stackoverflow.com/a/71174859/5173064. If I use your function and my arguments (created to really test the function) then your function throws error. BTW, the first part of your if will always be true: Object.keys(source).forEach(sourcekey=>{ if (Object.keys(source).find(targetkey=>targetkey===sourcekey) !== undefined – sicko Feb 18 '22 at 14:13
2

Small fine tune of phillips first answer.

const object1 = {
  abc: {
    a: 1
  }
};

const object2 = {
  abc: {
    b: 2
  }
};

Object.assign(object1, {
    abc: {
        ...object1.abc,
        ...object2.abc
    }
});

console.log(object1);
// Object { abc: Object { a: 1, b: 2 } }
Ballpin
  • 217
  • 2
  • 6
1

A working version which supports nested Arrays as well

const target = {
   var1: "...",
   var2: {
       nestVar1: "...",
       nestVar2: [...]   
       }
}

const source = {
  var2: {
      nestVar3: { ... }
      nestVar4: "...",
      nestVar5: [...]
      },
  var3: "...",
  var4: [...]
}

the Function:

function objectAssign(target, source) {
    Object.keys(source).forEach(sourceKey => {
      if(Object.keys(source).find(targetKey => targetKey === sourceKey) !== undefined && typeof source[sourceKey] === "object") {
        target[sourceKey] = objectAssign(target[sourceKey], source[sourceKey]);
      } else if(!Array.isArray(source)) {
        target[sourceKey] = source[sourceKey];
      } else {
        target = source;
      }
    });
    return target;
}

then

let newObj = objectAssign({}, target, source);
zEn feeLo
  • 1,877
  • 4
  • 25
  • 45
0

Here is my NestedObjectAssign() contribution, because I have seen a function being duplicated across posts, and it does not work good.

This version just works as expected, as shown in example:

function NestedObjectAssign(target, source) 
{
    Object.keys(source).forEach(sourceKey => 
    {
        if (typeof source[sourceKey] === "object") 
        {
            target[sourceKey] = NestedObjectAssign(target[sourceKey] || {}, source[sourceKey])
        }
        else 
        {
            target[sourceKey] = source[sourceKey]
        }
    })

    return target
}

var target = 
{
    stringTarget: "target",
    stringCommon: "targetCommon",
    nestedCommon: 
    {
        target: 1,
        common: 1,
        nestedTarget: 
        {
            target: 1,
            common: 1
        }
    }
}

var source = 
{
    stringSource: "source",
    stringCommon: "sourceCommon",
    nestedCommon: 
    {
        source: 2,
        common: 3,
        nestedSource: 
        {
            source: 2,
            common: 3
        }
    }
}

var t = NestedObjectAssign({}, target)
var s = NestedObjectAssign({}, source)
var targetSource = NestedObjectAssign(t, s)

console.log({ target })
console.log({ source })
console.log({ targetSource })
sicko
  • 157
  • 3
  • 4
0

I know it's an old question & there are many answers, but for future developers that will see this question there is a more straightforward solution for this issue: we'll need to convert custom objects into JSON objects.

Solution:

JSON.parse(JSON.stringify(YourCustomObjectHere));

This will string your custom object into JSON and then reparse it into JSON object.

I hope this answer helps all developers stuck in this issue & thanks

ABDO-AR
  • 160
  • 3
  • 9
  • 1
    That is nothing but a »poor mans deep clone«, not talking about performance at all, this will only work as expected if the object you pass in is completely serializable to json. That doesn't inlcude functions, classes, Dates, Class Instances that utilize the prototype chain and much more. – philipp Mar 28 '23 at 09:00
  • Thanks, I've just noticed that I was only needed it to store it in Firebase Firestore so I didn't noticed that the functions are not assigned to the class after parsing it from the JSON string – ABDO-AR Mar 28 '23 at 20:14