-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Normative: Make array spread accept nullish values #1069
Conversation
Previously, `[42, ...undefined]` threw a TypeError exception. The object spread proposal has the more useful behavior of silently ignoring `null` and `undefined` values, e.g. `{ a: 42, ...undefined }` results in `{ a: 42 }` instead of throwing an exception. This patch applies the same concept to array spread, making `[42, ...undefined]` result in `[42]`.
483823e
to
b61ec82
Compare
My immediate reaction here is that this is an error and I would like the error to be caught as close to the point where I would fix it as possible. A user can always spread |
I find it quite strange (aka surprisingly inconsistent) that If we relaxed the errors and let all of those forms fail silently, at least we'd have some consistency there. |
Does anyone remember why |
It was changed in ES6 draft rev27 per discussion in the July 29 2014 TC39 meeting. |
The key seems to be,
@sebmarkbage Do you have any information from these libraries about why they made that decision? |
We could also make this change in Let’s discuss this at next week’s meeting! Do we need a separate agenda item, or does this fall under the “needs consensus” PR topic? |
@mathiasbynens I've been trying to put needs-consensus PRs that I want to discuss on the agenda in advance, so it's easier to for committee members to look into the issue ahead of time. |
@littledan I suspect that the primary reason this patterned evolved to begin with was because this was how you implemented it: function assign(target, source) {
for (var key in source) {
...
}
} And since It turns out to be very useful since it comes up very frequently that you have optional additional properties such as a configuration object. When I suggested in circles of early spread users that it could be more restrictive than Treating it as an empty set of keys is convenient and also not a particularly confusing semantic. What else could it mean? Sure, it might cover up a mistake but so can many other things. For static type systems it is not really a problem because they'll instead catch it at the resulting type if there's a problem. For Flow specifically it would probably be worse to have the The mental model here is that it is not property access. It is key extraction followed by property access. The property access is consistent with how destructuring and defaults work. The key extraction just happens to result in no keys, and therefore there isn't a property access and therefore no error. The same rationale applies to why own/enumerability differ from destructuring. For arrays, it is a bit different because the equivalent of "key extraction" is extracting the iterator. You could argue that extracting the iterator from |
In general, I think that if two patterns evolve, we should choose the most permissive solution that allows both patterns to co-exist instead of trying to kill one. For object spread, there is another interesting property. All other primitive values (number, string, symbol, boolean) also gets treated as having an empty set of keys since their ToObject forms have no own/enumerable properties. That is not the case for array spread, since strings have iterators. |
What about |
I would absolutely expect |
What about relaxing the need for let { x, y } = obj || {}; and both of the function foo({
x: { a, b } = {}
} = {}) { .. } And of course the If the motivation of relaxing FWIW, in my code I have to write far more guard clauses in my destructuring than in my spreading. It also seems that the concern of "what if I expect/prefer exceptions on null or undefined?" equally applies to |
Can someone explain to me _how_ this change would be made?
I’m not the most well-versed in terms of then nature of what changes can and cannot be made (i.e.: in what cases we consider a change to be breaking).
My understanding was that with each version/draft of changes there is a strong commitment to making changes non-breaking, but surely some code since 2015 has come to rely on this behavior, even as a fallback, regardless of whether it is “correct”?
An example of where this code could be considered to be breaking (and for that matter, where throw-expressions could be considered to be breaking) is the following example:
```javascript
function shallowCopyArray(arr) {
try {
return [...arr];
} catch (e) {
return arr;
}
}
```
or something of that nature, regardless of what the catch behavior returns (and regardless of how silly this code itself actually is).
This code currently traps undefined, null, and whatever else is not able to be spread by nature of the response its `Symbol.isConcatSpreadable` method.
Changing this behavior to allow undefined to be spread would result in this not trapping a raised exception at all, but rather returning an empty Array as opposed to undefined.
|
@benderTheCrime I think one generally accepted exception (pun intended) or caveat to the non-breaking commitment is that it's seen as OK to relax (eliminate) an exception/error where it used to be thrown and now isn't (for any of a variety of reasons). The reason this still qualifies as non-breaking is it's not really guaranteed that exceptions are constant across spec revisions. Note: The reverse is (usually) not allowed: introducing an exception where none used to be. |
Hey, sorry @getify, I elaborated on my example a little bit, where an exception is raised/thrown as a direct byproduct of the code, but actually caught by some catch behavior that has been delegated. Is that still considered a non-breaking change regardless of the cases where this function will now behave very differently? |
I do, however, believe that the spec should be very explicit about the fact that the behavior of the spread operator changes based on what is being spread into and not what is being spread (if it is not already sufficiently explicit to this effect).
|
@benderTheCrime I think there's a difference between "breaks my code" and "breaks my code in a way we promised it wouldn't break". There's lots of scenarios that can be created, like the one you suggest, where theoretically someone's code could be broken. But ultimately they would all have been relying on an exception that wasn't necessarily and explicitly guaranteed to always be thrown. Think of it this way: we guarantee that a thing will always be there (once it's there), but we don't guarantee the inverse: a thing that's not there will never be there. Moreover, an overriding principle is that no change, whether justifiable with the above reasoning or not, sticks if it turns out that it breaks "enough" existing code that browsers refuse to implement it. The definition of "enough" varies a fair bit between browsers and issues at hand. So ultimately, even if we decided we wanted to do as discussed in this thread, if it turns out that it's actually (not just theoretically) breaking "enough" code to matter, if will be backed out. |
@benderTheCrime in the code you have, anything with a next() that throws would trigger the catch too; your code isn’t verifying that it’s an array (only |
So does a change like this follow the regular proposals process? Or are people going to find out about this when their production JavaScript starts breaking?
…Sent from my iPhone
On Jan 18, 2018, at 12:02 PM, Jordan Harband ***@***.***> wrote:
@benderTheCrime in the code you have, anything with a next() that throws would trigger the catch too; your code isn’t verifying that it’s an array (only Array.isArray can do that), it’s just verifying that arr is an object with an iterator that doesn’t throw. Changing what builtin values are in that set doesn’t break the guarantee your code has.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
It’s on the agenda to discuss at this month’s meeting. If you can point at actual non-contrived places in existing code where it would be breaking, that’d be helpful. |
For me, the mental model of
I don't find the argument about not wanting to use Changing the behavior of for (const foo of myObject.elments /* oops, typo */) {} Currently, this code will throw an error, making the bug obvious. However, the code would be very difficult to debug if it acted the same as an empty array, because it would be swallowing the error and implicitly replacing it with a seemingly-valid result. I don't think this would be an improvement for developer experience. |
@getify 's suggestion here is interesting. Does anyone remember why we decided in ES6 that we shouldn't allow iteration over undefined? @not-an-aardvark 's argument is somewhat compelling, but I can see both sides here. I had trouble finding something in the tc39-notes repository going back to 2012; not sure if this is in earlier notes. cc @allenwb |
At the January TC39 meeting, there was strong opposition to changing the way object spread works. There was some opposition to changing the way array spread or iteration works due to lack of use cases. Consistency by itself is not enough of an argument. |
I present a survey of all internal uses of the iteration protocol and their handling of
|
|
@raulsebastianmihaila Thanks! Updated the table. |
I think spread behaviors (objects and arrays) should be consistent. As others have mentioned, the error case here seems odd given the behavior of This should be viewed with the lens of consistency with spreads and not with the lens of consistency with the iteration protocol. I consider the fact that spreading in arrays can error in this way to be a spec bug (so FWIW I'm 👍 on this PR). If there's no general agreement that this is a spec bug fix, then should it proceed as any other syntax proposal with its own proposal repo, tc39 agenda item, and so on? |
@jdalton This was already brought in front of the committee during this week's TC39 meeting and did not achieve consensus because, among other things, consistency with object spread was not a compelling argument. I suggested that there may be changes needed to reach consistency among other uses of the iteration protocol, which is why I did the survey I posted above, but that turns out to not require any changes either. |
It should be compelling enough if TC39 folks are thinking with their dev/user hats on 😞 |
After discussing this at TC39 and especially after perusing @michaelficarra’s table I’m less convinced that we should change anything here.
|
Looking at the table, it seems more reasonable to throw a TypeError when object-spreading |
@gibson042 The committee consensus is that object spread must continue to match |
The behavior of those constructors is primarily there to handle to 0 arguments cases like |
That committee consensus is at odds with both the already-specified array spread and apparent developer consensus (not that either are news to you, it's your PR and your survey). JS won't die from one more inconsistency, but it does seem a bit wrong for syntax constructs (as opposed to function calls) to swallow exceptions. I mean, it's not like the |
Object spread is intended to match As for syntax, |
Can you elaborate? Data from the community seems to oppose that position (and is at best mixed), and anyway the exact match you desire is already missing: // throws
let a = Object.assign({ set x(v) { throw v } }, {x: 0});
// copies properties
let b = { set x(v) { throw v }, ...{x: 0} };
|
This discussion seems moot because TC39 has already reached a consensus on the question. But, for the record:
Object.assign's behavior was designed to exactly match what it would be if a for-in loop that skipped inherited properties was used for the property iteration. That choice was made because similar functions in several popular frameworks were implemented using such for-in loops. The assumption was that over time those functions would be replaced by Object.assign or reimplemented using Object.assign and we wanted to ensure compatibility with the previous implementations. I personally see no reason why compatibility with a single (and quite new) built-in function should have such an impact upon the design of a syntactic operator. If I had been designing the object spread operator consistency with array spread would have driven my decision, not consistency with Object.assign. Consistency among functions: important. Consistency among operators: important. Consistency among operators and functions that break the other important consistencies: mistake. |
@allenwb Thanks for the clarification. Regardless, the operator isn't actually "array spread" - it's "iterable spread". Iterability is determined by looking up a Symbol property on an object, which throws on null/undefined; "object spread" doesn't have to do that since there's no property lookup. I don't see this as a mistake nor as an inconsistency. |
With all due respect (and as @jdalton opined), that sounds like viewing things through spec-colored glasses rather than through the glasses of typical/general end-user developers. I interact regularly with such general developers of all skill levels, and I can say with strong certainty that they're not particularly thinking about the details of The TC39 consensus may indeed be that this inconsistency is acceptable (aka, better than alternatives), but I think it's unreasonable to take the position that it's not an inconsistency and rather the fault of general developers not studying deeply enough the internals of the two implementations. The differences between There's not usually a need for such developers to go to that level of detail. Such nuances usually only surface to the general end-user as a result of leaky design. |
Object spread doesn't have to lookup I believe it was, but like I said, JS won't die from one more inconsistency. |
My take away from the committee was that consistency alone wasn't enough of an argument (especially given other consistency issues such as the ones mentioned here and with how other primitives work). It seems like this thread is stuck on the consistency argument. That's not to say that there can't be other arguments for making this change. Such as that it is useful. It is possible to relax many of the existing errors too if it is useful to do so. I haven't seen explorations into what examples leave you with One interesting artifact is that the form Are there other inconveniences that would motivate this change? |
Related, I found myself wrapping |
@jdalton Could you point to a case where this came up for you? EDIT: Oops, I didn't scroll down enough, the example is right there. |
For the |
Could you elaborate on why you're spreading a value if you're not sure whether it's nullish? For example, it seems like you could also get an error if the value is an empty object rather than an array. It seems like the general solution would be to ensure that you only use array-spread on values which are actually iterable. |
It's not something I had considered a gotcha. It turns out I also had loose mode enabled 😋 as a blanket setting. If I start a project without transpiling then I'll likely have to throw in |
I just his this issue when trying to convert some code that used concat. I was tempted to write: [...listOne, ...listTwo] but had to write this instead: [...listOne || [], ...listTwo || []] Compared to being able to just write If we can't agree to make spread null-safe, can we add a null-safe version of spread as part of the optional-chaining proposal? Maybe |
that also assumes one remember precedence of the [...(listOne || []), ...(listTwo || [])] I agree |
That's how I write it and I dislike it, syntax-wise and performance-wise. Maybe it gets optimized away, but what happens here is
Whereas, if the array spread syntax would ignore
|
I’m gonna close this, as the suggestion didn’t get committee consensus. Happy to continue the discussion in the closed issue though. |
Ran into this just recently, having somehow managed to forget that iterable spread doesn't special case doSomething(["hardcoded1", "hardcoded2", ...more]); ...where Like others here, I don't like my choices for dealing with it:
I would much prefer iterable spread to special case |
Previously,
[42, ...undefined]
threw a TypeError exception.The object spread proposal has the more useful behavior of silently ignoring
null
andundefined
values, e.g.{ a: 42, ...undefined }
results in{ a: 42 }
instead of throwing an exception.This patch applies the same concept to array spread, making
[42, ...undefined]
result in[42]
.