How Spread Syntax Breaks Javascript

I haven’t looked at the mailing lists and drafts, so I’m not sure who came up with the brilliantly-awful spread syntax (not operator) that’s made its way into ES6 and is being used liberally by React, Redux, Vue, and many others, but… seriously? Ecmascript has been making strides in the “serious language for serious things” arena, but stuff like this just brings it back to the joke status it used to have. In this post, I’ll explore how (and why) spread syntax (...S) is confusing, semantically ambiguous, and, in a few cases, even breaks expected behavior.

This post is not introductory. If you are unfamiliar with rest syntax, I suggest checking out a tutorial or guide prior to diving in my discussion.

Semantics

Generally speaking, it’s not a smart idea to have similarly-looking language constructs that mean different things. One of the major criticisms of overloading operators is that it can be semantically confusing. In fact, one of the tenets of HICPP is do not overload operators with special semantics. In other words, we all have a pretty clear idea what + is supposed to do when we see it. If we overload this additive behavior, it makes our code difficult to debug and almost impossible to understand. And yet, this is exactly what spread syntax does. Spread syntax has behavior that will vary depending on the context: array destructuring, object destructuring, array merging, object merging, and arrow functions.

Destructuring

Take a look at this simple example:

arr = [ 'a','b','c' ];
obj = { A: 'hello', B: arr}
var { A, B } = obj;
console.log(B)

Let’s see how the behavior can vary wildly (and confusingly):

var { A, B } = obj;
console.log(...B)

Because B is an array, we destructure it successfully, returning a b c.

var { A, ...B } = obj;
console.log(...B)

Here, the first ...B does not destructure, instead it acts as ...rest, so we have B actually turning into an object containing all top-level properties of obj except for A. We can’t destructure non-iterables, so doing ...B (the second time) fails.

var { A, ...B } = obj;
console.log(B)

This works, but B contains an array called B. How confusingly fun.

Not all Rests are Created Equal

So what’s the difference between a ...rest when destructuring an object and a ...rest when declaring a function?

{ prop1, prop2, ...rest } = obj;  // destructuring
(arg1, arg2, ...rest) => true     // in a function

The first rest will be an object, while the latter will be an array. This is extremely confusing, and goes against intuition given cases like this:

arr = [ 'a','b','c' ];
obj = { ...arr, hello: 'world' }
var { hello, ...R } = obj;
console.log(R) // Object(3) [ "a", "b", "c" ]

All that’s left in obj is an array, so why isn’t R just an array? Further, if we use spread syntax in function declarations, we actually lose all type information. ...rest becomes a vanilla array, not the Arguments array-like object which we would get by looking at, e.g., the arguments variable. Why? Argument collections should be consistent.

Finally, what do you think this does?

C = { hello: 'world' }
D = { A: 'bar' };
({ A, ...B } = { ...C, ...D });

You might have thought “oh okay, whenever I see a ...something at the end of an object, I know it’s a rest component,” but that would be wrong. ...B is actually the ...rest component, while A is the destructured A from D and both ...C and ...D are simply the properties of C and D respectively.

Syntax

Next, let’s look at how spread syntax is confusing syntactically and how, in a contrived, but nevertheless relevant case, it breaks expected behavior.

Ternary Troubles

Given the following example, we might be able to intuit what the behavior will be:

A = undefined
console.log(...A)
// TypeError: A is undefined

But what about this? What does this code do?

A = undefined
console.log(...A ? true : false)

To the programmers’ confusion, we get TypeError: false is not iterable. Wait.. what? We get this cryptic error precisely because ... is not an operator. Instead, the code above can be re-written (more accurately) as:

A = undefined
console.log( ...(A ? true : false) )

And it can actually run if we stick true and false into arrays:

A = undefined
console.log( ...(A ? [true] : [false]) )
// we get false because A is undefined

Wow, that’s pretty confusing (not to mention surprising). But I guess it’s just a quirk of the language, right? Who doesn’t have endless hours (like this blog author) to dig through specs and understand these fun idiosyncrasies?

Parentheses are Fun

Remember when parentheses were used for grouping things? When they didn’t affect what you’re actually doing at all and only made code easier to read?

A = 'hello world'
console.log( A )   // prints 'hello world'
console.log( (A) ) // also prints 'hello world'

No more! Time to introduce a new age where shoehorned functional programming paradigms and confusing syntax like ... break everything!

A = ['hello', 'world']
console.log( ...A )   // prints 'hello world'
console.log( (...A) ) // SyntaxError: expected '=>' after argument list, got ')'

Oh, that’s right, (...A) is ...rest for an arrow function’s arguments. What a fun surprise!

Breaking the Comma Operator

But here’s a fun edge case where ... syntax actually breaks the comma operator. In fact, I’m not even sure how to make this thing run (maybe one of you can help). Consider this code:

A = ['hello', 'world']
B = ['some', 'array']
console.log( A[1, 'some', 'array', 0]) // works as expected, returns 'hello'
console.log( A[1, ...B, 0]) // SyntaxError: expected expression, got '...'

I’m destructuring the exact same array elements, namely some and array but ... syntax does not play well with the comma operator.

FIXME

I’m not sure exactly how you’d go about fixing ... syntax, but here are some thoughts.

First of all, I’d probably make it a full-fledged operator, so order of operations semantics will apply. At least, we won’t be confused by things like the ternary operator. Second, I would use different syntax for ...rest behavior. It’s extremely confusing in some cases (see Example 7). Finally, it would be nice if ...[a, b, c] would be just syntactic sugar for a, b, c — but making it have odd context-specific behavior is confusing and, in this writer’s humble opinion, detrimental to the language as a whole.