< back home

Tips & Tricks: Object Spread

This article was originally published on #dev-a-day.

Here's the setup: you have an object and you want to assign a new property on that object to some value. Here's the vanilla JS way to go about it:

function setBar(obj) {
  obj.bar = 'bar';
  return obj;
}

var obj = { foo: 'foo' };
setBar(obj);
// {
//   foo: 'foo',
//   bar: 'bar',
// }

This isn't perfect though, as it's best practice for functions to have no side effects; when you call a function, it should do what it explicitly states that it does and nothing else. In JS, objects are passed by reference, so when you call a function with an object as a parameter, you're just passing a reference to that object to the function - this means that any modifications you make to the object from within the function will modify the original object, which can lead to some unintended consequences down the road.

function setBar(obj) {
  obj.bar = 'bar';
  return obj;
}

var obj = { foo: 'foo' };
setBar(obj);
// obj.bar === 'bar'

Avoiding Side Effects

If we want to avoid this, we need to create a copy of the object parameter(s) inside the function body and return that instead of mutating the argument object directly. The modern JS spec has introduced Object.assign to assist with this process:

function setBar(obj) {
  const copy = Object.assign({}, obj);
  copy.bar = 'bar';
  return copy;
}

var obj = { foo: 'foo' };
var newObj = setBar(obj); // newObj.bar === 'bar'
// obj.bar === undefined

Object assignment enumerates over all the instance properties of each argument in sequence and applies them to the first argument (if you aren't assigning to an existing object, just pass an empty object {} as the first argument).

const obj = { foo: 'foo' };
const mod = Object.assign({}, obj); // { foo: 'foo' }
console.log(obj === mod); // false (mod is a new instance)

Object.assign({}, { foo: 'foo' }, { bar: 'bar' }, { baz: 'baz' });
// {
//   foo: 'foo',
//   bar: 'bar',
//   baz: 'baz',
// }

// properties later in sequence will overwrite previous argument properties
Object.assign({}, { foo: 'foo' }, { foo: 'bar' });
// {
//   foo: 'bar',
// }

ES Object Spread Operator

ECMAScript introduces the object spread operator ... to allow for enumerating over an object's properties inline. There are a few different ways to make use of it, from simple to a bit more complex.

Shallow Copy

It may be used to create a shallow copy of an object a la Object.assign:

const obj = { foo: 'foo' };
const mod = { ...obj };

A shallow copy will duplicate an object's values by reference, which means that mutating object values on a copy will still mutate the original object's properties as well:

const obj = { foo: { bar: 'bar' } };
const mod = { ...obj };

console.log(obj.foo.bar); // bar
mod.foo.bar = 'foo';
console.log(obj.foo.bar); // foo

If you want to instead create a completely independent copy of an object, you'll need to perform a deep copy, which you can either perform yourself or pull from a package such as deep-copy.

Inline Array Map

We can utilize the object spread operator to write a very minimal array mapping (when returning an object from an arrow function, make sure to wrap the object in parentheses to assert that it's not a function body like () => ({})):

const users = [
  { id: 1, first: 'Jane', last: 'Doe' },
  { id: 2, first: 'John', last: 'Doe' },
  { id: 3, first: 'Foo', last: 'Bar' },
];

console.log(
  users.map(user => ({
    ...user,
    name: `${user.first} ${user.last}`,
  })),
);
// [
//   { id: 1, first: 'Jane', last: 'Doe', name: 'Jane Doe' },
//   { id: 2, first: 'John', last: 'Doe', name: 'John Doe' },
//   { id: 3, first: 'Foo', last: 'Bar', name: 'Foo Bar' },
// ];

Variable Key Assignment

Varible key assignment allows setting an object property value inline. Traditionally this couldn't be accomplished inline and required using string-indexed array access syntax:

Traditional Method

// traditional method
const three = 3;
const calc = {};
calc[parseInt('1')] = 'one';
calc[1 + 1] = 'two';
calc[three] = 'three';
// calc === {
//   1: 'one',
//   2: 'two',
//   3: 'three',
// }

ES Method

// ES method
const three = 3;
const calc = {
  [parseInt('1')]: 'one',
  [1 + 1]: 'two',
  [three]: 'three',
};
// calc === {
//   1: 'one',
//   2: 'two',
//   3: 'three',
// }

By combining object spread and variable key assignment, we can create very powerful mapping functions in just a few lines (you'll recognize keyBy from the previous article about Reducers):

function keyBy(arr, key) {
  return arr.reduce(
    (map, obj) => ({
      ...map,
      [obj[key]]: obj,
    }),
    {},
  );
}

const users = [
  { id: 1, name: 'Jane Doe' },
  { id: 2, name: 'John Doe' },
];
keyBy(users, 'id');
// {
//   1: { id: 1, name: 'Jane Doe' },
//   2: { id: 2, name: 'John Doe' },
// }

Guarded / Conditional Key Assignment

Occasionally we want to assign one or more properties if a condition is true, or optionally a different one or more properties if the condition is false. Let's look at the traditional and ES methods of accomplishing this:

Traditional Method

const obj = {};

// guarded assignment
if (someCondition) {
  obj.foo = 'foo';
}

// conditional assignment
if (anotherCondition) {
  obj.bar = 'bar';
} else {
  obj.quz = 'quz';
}

ES Method

// guarded assignment
const obj = {
  ...(someCondition && {
    foo: 'foo',
  }),
};

// conditional assignment
const obj = {
  ...(anotherCondition
    ? {
        bar: 'bar',
      }
    : {
        quz: 'quz',
      }),
};

Conclusion

Object spread assignment is a great tool for expressing routine assignment logic in an idiomatic and concise way. Tomorrow we'll look at the other use case for ..., the parameter spread operator.