TypeScript/JavaScript three dots (Spread operator)

JavaScript/TypeScript

Three dots ... is called spread operator in Javascript and Typescript. This is useful when we want to

  • create a copy of a object
  • destruct an array and pass them to parameters
  • avoid same definition

Let’s learn how to use the spread operator.

Sponsored links

Creating a copy object

What the spread operator does is copy an object. It copies values in an object. Let’s see an example.

const obj = {
    foo: 123,
    hoge: 44,
    name: "obj-name",
};
console.log(obj);
// { foo: 123, hoge: 44, name: 'obj-name' }
const spread = { ...obj };
console.log(spread);
// { foo: 123, hoge: 44, name: 'obj-name' }
console.log(`original === spread: ${obj === spread}`);
// original === spread: false

The values in obj and spread variables are exactly the same but they are different objects as you can see the result at the end line. It means that the values in the original object are the same even though we change the values in the copied object.

const obj = {
    foo: 123,
    hoge: 44,
    name: "obj-name",
};
const spread = { ...obj };
spread.foo = 555;
spread.hoge = 999;
console.log(obj);
// { foo: 123, hoge: 44, name: 'obj-name' }
console.log(spread);
// { foo: 555, hoge: 999, name: 'obj-name' }

is specified more than once, so this usage will be overwritten.

We can easily extend an object by spread operator when we want to change one of the values and add additional properties.

const obj = {
    foo: 123,
    hoge: 44,
    name: "obj-name",
};
const newObj = { ...obj, added: "HEY" };
console.log(newObj);
// { foo: 123, hoge: 44, name: 'obj-name', added: 'HEY' }

const updatedObj = { ...obj, foo: "Updated foo" };
console.log(updatedObj);
// { foo: 'Updated foo', hoge: 44, name: 'obj-name' }

Spread operator can be specified wherever you want but the compiler complains if there is the same property name in front.

const obj = {
    foo: 123,
    hoge: 44,
    name: "obj-name",
};
// const opposite = { foo: "Updated foo", ...obj };
// 'foo' is specified more than once, so this usage will be overwritten.

const opposite = { newProp: 9999, ...obj };
console.log(opposite);
// { newProp: 9999, foo: 123, hoge: 44, name: 'obj-name' }

However, if using multiple spread operators compiler doesn’t complain about it. The second parameter just overwrites existing parameters.

const obj = {
    foo: 123,
    hoge: 44,
    name: "obj-name",
};
const obj2 = { foo: "second foo", age: 15, hobby: "soccer" };
const merged = { ...obj, ...obj2 };
console.log(merged);
// {
//   foo: 'second foo',
//   hoge: 44,
//   name: 'obj-name',
//   age: 15,
//   hobby: 'soccer'
// }

Passing values from array

Spread operator can be used for array as well.

const array = ["Hey", "Ho", "Hello"];
const array2 = ["Boo", "Yeah", "Oops"];
console.log([array, ...array2]);
// [ [ 'Hey', 'Ho', 'Hello' ], 'Boo', 'Yeah', 'Oops' ]
console.log([...array, ...array2]);
// [ 'Hey', 'Ho', 'Hello', 'Boo', 'Yeah', 'Oops' ]

There are several ways to get the same result. Array.prototype.concat() and Array.prototype.flat() for example. Following 3 statements return the same result.

  • array = [...array, ...array2, ...array3]
  • array.concat(array2, array3)
  • array = [array, array2, array3].flat()

Rest parameters. Use spread operator for function arguments

If we don’t know how many arguments are specified in a function we can use these three dots … . The argument must be an array in this case.

function doSomething(...args: string[]) {
    args.forEach((value: string, index: number) => {
        console.log(`${index}:${value}`);
    });
}
const strs = "HELLO";
const array = ["chair", "desk", "smartphone"];
doSomething(...strs);
// 0:H
// 1:E
// 2:L
// 3:L
// 4:O
doSomething(...array);
// 0:chair
// 1:desk
// 2:smartphone

We can’t pass an object with a spread operator. We get the following error in this case.

const obj = { foo: 11, hoge: 55 };
// doSomething(...obj); // error
// --- Error message  ---
// const obj: {
//     foo: number;
//     hoge: number;
// }
// Type '{ foo: number; hoge: number; }' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)

Object.assign() vs spread operator

Object.assign() basically does the same thing.

const obj = {
    foo: 123,
    hoge: 44,
    name: "obj-name",
};
// same as { ...obj }
const newObj = Object.assign({}, obj);
console.log(newObj);
// { foo: 123, hoge: 44, name: 'obj-name' }

const objToUpdate = { added: "HEY", foo: "Updated foo" };
// Same as { ...obj, ...objToUpdate }
const addedObj = Object.assign({}, obj, objToUpdate);
console.log(addedObj);
// { foo: 'Updated foo', hoge: 44, name: 'obj-name', added: 'HEY' }

console.log(`obj === result: ${obj === newObj}`);
// obj === result: false
const copyObj = { ...obj };
console.log(`obj === result: ${obj === copyObj}`);
// obj === result: false

However, we need to know that the behavior is a little different from the spread operator. It’s not suitable to use Object.assign() when an object has a getter/setter and we want to copy the values.

const person = {
    _name: "YUTO",
    set name(value: string) {
        this._name = "yuto";
    }
};
const department = {
    name: "R&D",
};
console.log({ ...person, ...department });
// { _name: 'YUTO', name: 'R&D' }
console.log(Object.assign(person, department));
// { _name: 'yuto', name: [Setter] }

The result is different because Object.assign() calls getter/setter.

Deep copy and shallow copy

Let’s check how it works when a property in an object has an object.

const originalObj = {
    value: "1",
    first: {
        value: "1-1",
        second: {
            value: "2-1",
            third: {
                value: "3-1"
            }
        }
    }
};
const copiedObj = { ...originalObj };
console.log(copiedObj);
// {
//   value: '1',
//   first: { value: '1-1', second: { value: '2-1', third: [Object] } }
// }

copiedObj looks the same as originalObj . Let’s update the values copiedObj and show the values of originalObj .

copiedObj.value = "1-updated";
copiedObj.first.value = "1-1-updated";
copiedObj.first.second.value = "2-1-updated";
copiedObj.first.second.third.value = "3-1-updated";
console.log(originalObj); // original!
// {
//   value: '1',
//   first: {
//     value: '1-1-updated',
//     second: { value: '2-1-updated', third: [Object] }
//   }
// }

The variable for the console.log is originalObj but its values changed because the spread operator does shallow copy. It copies only first-level property values. If the property is an object spread operator copies the reference value to the new variable. The copied object refers to the same place as the original object. See the following image.

It copies those values but those values for objects are references. You recognize that it’s a pointer if you know C, C++ for example. An object is a set of values and those values need to be placed in the near addresses. That’s why it refers to another place as shown in the image above.

Restoring process.env variable for unit testing

Some tests require process.env setting but once you change a value its change remains and causes test error. For this reason, initial values need to be set after each test case.

This is an example written with mocha module as a test runner.

let originalEnv: NodeJS.ProcessEnv;

before(() =>{
    originalEnv= { ...process.env };
});

afterEach(() =>{
    process.env = { ...originalEnv};
});

describe("Example", () => {
    it("should be something",()=>{
        // test case
    });
})

Variable Declaration by spread operator

The spread operator can be used to declare a new variable. When we want to declare a new variable used in an object, we can do it with a bracket.

const obj = {
    first: 1,
    second: 2,
    third: 3,
};
const { first, second, third } = obj;
console.log(first);  // 1
console.log(second); // 2
console.log(third);  // 3

If we want to extract only one property and keep other properties in another object we can use the spread operator.

const { second, ...rest } = obj
console.log(second); // 2
console.log(rest);   // { first: 1, third: 3 }

Summary

Spread operator copies top-level values. If the object has an object its value is just a reference. If we make a change to a copied object its change affects the original object as well.

Comments

Copied title and URL