Object Rest and Spread in TypeScript
TypeScript 2.1 adds support for the Object Rest and Spread Properties proposal that is slated for standardization in ES2018. You can work with rest and spread properties in a type-safe manner and have the compiler downlevel both features all the way down to ES3.
#Object Rest Properties
Let's assume you have defined a simple object literal with three properties:
const marius = {
name: "Marius Schulz",
website: "https://mariusschulz.com/",
twitterHandle: "@mariusschulz",
};
Using the ES2015 destructuring syntax, you can create several local variables that hold the values of the corresponding property. TypeScript will correctly infer the type of each variable:
const { name, website, twitterHandle } = marius;
name; // Type string
website; // Type string
twitterHandle; // Type string
That's all good and true, but nothing new so far. This is where object rest comes into play and enables another destructuring feature: In addition to extracting a set of properties you're interested in, you can collect all remaining properties in a rest element using the ...
syntax:
const { twitterHandle, ...rest } = marius;
twitterHandle; // Type string
rest; // Type { name: string; website: string; }
TypeScript will determine the correct types for all resulting local variables. While the twitterHandle
variable is a plain string, the rest
variable is an object containing the remaining two properties which weren't destructured separately.
#Object Spread Properties
Let's assume you want to use the fetch()
API to make an HTTP request. It accepts two parameters: a URL and an options object containing any custom settings that you want to apply to the request.
In your application, you might encapsulate the call to fetch()
and provide default options and the possibility to override specific settings for a given request. These options objects can look like this:
const defaultOptions = {
method: "GET",
credentials: "same-origin",
};
const requestOptions = {
method: "POST",
redirect: "follow",
};
Using object spread, you can merge both objects into a single new object that you can the pass to the fetch()
method:
// Type { method: string; redirect: string; credentials: string; }
const options = {
...defaultOptions,
...requestOptions,
};
Object spread will create a new object, copy over all property values from defaultOptions
, and then copy over all property values from requestOptions
— in that order, from left to right. Here's the result:
console.log(options);
// {
// method: "POST",
// credentials: "same-origin",
// redirect: "follow"
// }
Notice that the order of assignments matters! If a property appears in both objects, the later assignment wins. This is why defaultOptions
is listed before requestOptions
— if it was the other way around, there would be no way to override the defaults.
Of course, TypeScript understands this ordering. Therefore, if multiple spread objects define a property with the same key, the type of that property in the resulting object will be the type of the property of the last assignment because it overrides previously assigned values of that property:
const obj1 = { prop: 42 };
const obj2 = { prop: "Hello World" };
const result1 = { ...obj1, ...obj2 }; // Type { prop: string }
const result2 = { ...obj2, ...obj1 }; // Type { prop: number }
In a nutshell: later assignments win.
#Making Shallow Copies of Objects
Object spread can be used to create a shallow copy of an object. Let's say you want to create a new todo item from an existing one by creating a new object and copying over all properties. With object spread, that's a one-liner:
const todo = {
text: "Water the flowers",
completed: false,
tags: ["garden"],
};
const shallowCopy = { ...todo };
And indeed, you get a new object with all property values copied:
console.log(todo === shallowCopy);
// false
console.log(shallowCopy);
// {
// text: "Water the flowers",
// completed: false,
// tags: ["garden"]
// }
You can now modify the text
property without changing the original todo item:
shallowCopy.text = "Mow the lawn";
console.log(shallowCopy);
// {
// text: "Mow the lawn",
// completed: false,
// tags: ["garden"]
// }
console.log(todo);
// {
// text: "Water the flowers",
// completed: false,
// tags: ["garden"]
// }
However, the new todo item references the same tags
array as the first one. No deep clone was made! Therefore, mutating the array will impact both todos:
shallowCopy.tags.push("weekend");
console.log(shallowCopy);
// {
// text: "Mow the lawn",
// completed: false,
// tags: ["garden", "weekend"]
// }
console.log(todo);
// {
// text: "Water the flowers",
// completed: false,
// tags: ["garden", "weekend"]
// }
If you want to create a deep clone of a serializable object, consider JSON.parse(JSON.stringify(obj))
or some other approach. Just like Object.assign()
, object spread only copies over property values, which might lead to unintended behavior if a value is a reference to another object.
Note that none of the code snippets in this post contain any type annotations or other TypeScript-specific constructs. It's just plain JavaScript mixed with the proposed object rest syntax. Type inference for the win!
This article and 44 others are part of the TypeScript Evolution series. Have a look!