DEV Community

Cover image for A spread, a rest and empty values
Eryk Napierała
Eryk Napierała

Posted on • Edited on

12 3

A spread, a rest and empty values

Three dots syntax (...) became quite popular in JavaScript world in the last years. It's used for a couple of different things: object and array spread, destructuring and rest arguments. In each case, the same part keeps tricky or at least not quite intuitive - empty values. What if you want to spread an array that appears to be undefined? What about destructuring a null object?

Object spread

const foo = { ...bar, baz: 1 };
Enter fullscreen mode Exit fullscreen mode

Spreading an object is quite a common pattern when you want to create one object based on another. In the above example, we're creating object foo by taking all the properties of bar, whatever it contains, and setting one particular property baz to 1. What if bar turns out to be undefined or null?

const bar = undefined;
const foo = { ...bar, baz: 1 };
console.log(foo);
Enter fullscreen mode Exit fullscreen mode
{ baz: 1 }
Enter fullscreen mode Exit fullscreen mode

The answer is: nothing bad happens. The JavaScript engine handles this case and gracefully omits a spread. The same goes for null, you can check it by yourself. That was easy!

Object destructuring

const { baz, ...bar } = foo;
Enter fullscreen mode Exit fullscreen mode

Destructuring an object is handy when dealing with nested data structures. It allows binding property values to names in the scope of the function or the current block. In the example above two constant values are created: baz equal to the value of foo.baz and bar containing all other properties of the object foo (that's what is called "a rest"). What happens when foo is an empty value?

const foo = undefined;
const { baz, ...bar } = foo;
console.log(baz, bar);
Enter fullscreen mode Exit fullscreen mode
Uncaught TypeError: Cannot destructure property 'baz' of 'foo' as it is undefined.
Enter fullscreen mode Exit fullscreen mode

In this case, the JavaScript engine gives up and throws a TypeError. The issue here is, that non-object value (and everything except null and undefined is an object in JavaScript), simply cannot be destructured. The issue can be resolved by adding some fallback value to the statement, so the destructuring part (the left one) always gets an object.

const { baz, ...bar } = foo || {};
Enter fullscreen mode Exit fullscreen mode

This kind of error usually occurs when destructuring function arguments or nested objects. In such a case, instead of || operator, we can use a default parameter syntax. A caveat here is not handling the null value. Only undefined will be replaced with an empty object.

function foo({
  baz: {
    qux,
    ...bar
  } = {}
} = {}) {
   // ...
}
Enter fullscreen mode Exit fullscreen mode

Array spread

const foo = [ baz, ...bar ];
Enter fullscreen mode Exit fullscreen mode

Similarly to the object, we can create an array based on the other. At first sight, the difference is only about the brackets. But when it comes to empty values...

const bar = undefined;
const foo = [ ...bar, 1 ];
console.log(foo);
Enter fullscreen mode Exit fullscreen mode
Uncaught TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
Enter fullscreen mode Exit fullscreen mode

Unlike object spread, the array spread doesn't work for null and undefined values. It requires anything iterable, like a string, Map or, well, an array. Providing such a value as a fallback is enough to fix the issue.

const foo = [ ...(bar || []), 1 ];
Enter fullscreen mode Exit fullscreen mode

Array destructuring

const [ baz, ...bar ] = foo;
Enter fullscreen mode Exit fullscreen mode

Array destructuring is no different - the destructured value must be iterable.

const bar = undefined;
const [ baz, ...bar ] = foo;
console.log(baz, bar);
Enter fullscreen mode Exit fullscreen mode
Uncaught TypeError: foo is not iterable
Enter fullscreen mode Exit fullscreen mode

Again, the remedy may be || operator or the default argument value when it's about destructuring function parameters.

const [ baz, ...bar ] = foo || [];
Enter fullscreen mode Exit fullscreen mode
function foo([
  [
    baz,
    ...bar
  ] = []
] = []) {
   // ...
}
Enter fullscreen mode Exit fullscreen mode

To sum up - when it comes to destructuring things, we have to ensure there is always something to destructure, at least an empty object or array. Values like null and undefined are not welcome.

Rest arguments

function foo(bar, ...baz) { return [bar, baz]; }
Enter fullscreen mode Exit fullscreen mode

In JavaScript, ... may be found in one more place - a function definition. In this context, it means: whatever comes to the function after named arguments, put it into an array. In the above example, bar is a named argument of the foo function and baz is an array containing all the rest of values.

What happens when exactly one argument comes to the function or when it's called with no parameters? Is that an issue at all?

foo(1);
Enter fullscreen mode Exit fullscreen mode
[1, []]
Enter fullscreen mode Exit fullscreen mode

It is not! JavaScript engine always creates an array for the rest arguments. It also means that you can safely destructure this value without providing a fallback. The code below is perfectly valid and it's not going to fail even when foo is called without arguments.

function foo(...bar) {
   const [baz, ...qux] = bar;
}
Enter fullscreen mode Exit fullscreen mode

Extra - JSX property spread

const foo = <div {...bar} baz={1} />;
Enter fullscreen mode Exit fullscreen mode

JSX is not even a JavaScript, but it shares most of its semantics. When it comes to spreading the object on the React element, empty values behave just like for object spread. Why is that so?

The code above means: create <div> element with a single property baz equal to 1 and all the properties of the object bar, whatever it contains. Does it sound familiar? Yes! It's nothing more than an object spread.

const fooProps = { ...bar, baz: 1 };
Enter fullscreen mode Exit fullscreen mode

When compiling JSX down to JavaScript, Babel uses old-fashioned Object.assign function and does not create an intermediate variable, but the final effect is the same.

const foo = React.createElement("div", Object.assign({ baz: 1 }, bar));
Enter fullscreen mode Exit fullscreen mode

So the answer is: null and undefined values are just fine when spreading on a React element. We don't need any checking or fallback values.

The snippet

You may wonder what is the result of calling a function presented on the cover photo of this article.

function foo({ bar, ...baz }, ...qux) {
    const [quux, ...quuux] = bar;
    return [{ qux, ...quux }, ...quuux];
}

foo(undefined);
Enter fullscreen mode Exit fullscreen mode

It fails immediately on destructuring the first argument, as object destructuring requires at least an empty object. We can patch the function adding a default value for the argument.

function foo({ bar, ...baz } = {}, ...qux) {
Enter fullscreen mode Exit fullscreen mode

Now it fails on destructuring of bar as it's undefined by default and that's not an iterable thing for sure. Again, specifying a default value helps.

function foo({ bar = [], ...baz } = {}, ...qux) {
Enter fullscreen mode Exit fullscreen mode

In this form, the function works perfectly for undefined. What about null? Unfortunately, providing a fallback to both null and undefined requires at least || operator. The function definition becomes far less concise.

function foo(barBaz, ...qux) {
    const { bar, ...baz } = barBaz || {};
    const [quux, ...quuux] = bar || [];
    return [{ qux, ...quux }, ...quuux];
}
Enter fullscreen mode Exit fullscreen mode

And that's fine only when you don't care about other falsy values like an empty string or 0. A more safe solution would be a ternary expression like barBaz == null ? {} : barBaz. Things turn complicated.

Conclusion

Be careful when using three dots syntax with values that you're not sure about, like ones that come from backend API or third party libraries. If you're about destructuring an object or array (or spreading an array), always check against null and undefined and provide a fallback value.

In many cases, using optional chaining syntax may produce much more readable code. Check out the performance of this syntax here.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site