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.