Repository navigation
deleteCount param to splice() is not optional in es5 types #32638
Description
Activity
ES5 Array.prototype.splice (start, deleteCount, ...
- Let actualDeleteCount be min(max(ToInteger(deleteCount),0), len – actualStart).
- Let number be the result of calling ToNumber on the input argument.
Argument Type Result Undefined NaN - If number is NaN, return +0.
max(ToInteger(undefined),0)is justmax(+0, 0)is just zeroRyanCavanaugh commented
on Jul 31, 2019 MemberMore actionsUnless I'm confused about something, the function's behavior is well-predictable when given one argument.
The spec is not authoritative as to argument optionality since it's not a well-defined concept in JS and
ToInteger(undefined)gives0which is a good default argument in many cases.For example
Number.prototype.toFixedlists its argument as required, but TypeScript doesn't enforce this because(0).toFixed()is a well-formed and predictable result. Same forString.prototype.slice- its arguments are both listed as "required" but has predictable and expected behavior with one.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jul 31, 2019 tjenkinson commented
on Jul 31, 2019 ContributorAuthorMore actionsHmm interesting. The reason I opened this is because we had a
.splice(0), and then our app broke in an old version of opera because there the second argument defaulted to 0 (correctly for es5) instead of the array length (explicit in es6).The issue is the default value essentially changed between es5 and es6.
Given the the default becomes something else in es6, and the param is not marked as optional in es5, do you think it makes sense for there to be an exception in this case?
It's not safe to omit
deleteCountunless you know for sure what environment you are running in.The wording used in the spec is interesting:
When the
splicemethod is called with two or more arguments [...]Based on this, and the fact that the behavior changed between ES5 and ES6 (despite "don't break the web" being the unofficial ES motto), the default value of 0 therefore seems to just be an accident of the integer coercion rules and not directly intended in the design. It definitely seems like the older spec wants to treat the second parameter as required, at any rate.
That said, even with the reasoning above, the fact that in practice there is a "default" and it isn't consistent between ES5 and ES6 is quite frustrating.
Yeah, after reading more, it seems like making the 2nd arg required for ES5 and optional for ES6 would be better in this particular case.
I don't think I've ever called splice without a second arg, though, because it's not very useful.
The ES6 spec explicitly mentions behaviour for 1 Arg being passed,
- Else if the number of actual arguments is 1, then
Let insertCount be 0.
Let actualDeleteCount be len – actualStart
But it seems like the ES5 spec is hinting that if called with zero or one Arg, then the behavior is more or less undefined, even if the algorithm outlined defines a behavior
- Else if the number of actual arguments is 1, then
- addedBugA bug in TypeScriptA bug in TypeScriptand removedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jul 31, 2019 - addedDomain: lib.d.tsThe issue relates to the different libraries shipped with TypeScriptThe issue relates to the different libraries shipped with TypeScript
on Jul 31, 2019 RyanCavanaugh commented
on Jul 31, 2019 MemberMore actionsOK, that sounds pretty annoying. Accepting PRs
RyanCavanaugh commented
on Jul 31, 2019 MemberMore actionsThanks for the feedback on this BTW. When/if someone submits a PR it'd be nice if this issue had a clear and concrete description of what happens in ES5 vs ES6 with the various argument counts.
Reacted by Tom Jenkinsontjenkinson commented
on Jul 31, 2019 ContributorAuthorMore actionsAwesome. I will put together a PR.
I know I have used
splice(0)several times in the past. It would have been great to have typescript flag it then :)I think this is what happens.
Maybe someone can correct me if I made a mistake.
ES array.splice() array.splice(start) ES5 (0, 0)(start, 0)ES6 (0, array.length)(start, array.length-actualStart)*
From this, the
actualStartis computed,- Let
relativeStartbeToInteger(start). ReturnIfAbrupt(relativeStart).- If
relativeStart < 0, letactualStartbemax((len + relativeStart),0); else letactualStartbemin(relativeStart, len).
Reacted by Tom Jenkinson- Let
- added 2 commits that reference this issue
on Jul 31, 2019 tjenkinson commented
on Jul 31, 2019 ContributorAuthorMore actionsPR: #32643
- added a commit that references this issue
on Mar 12, 2020 Hi! I've run into a related issue. I'm not sure if this is really a bug with TypeScript, the interpreters, or the spec itself. However, the typings are at the very least misleading. ES2022 deleteCount was clarified to be clearly optional, but if you pass
undefinedas deleteCount splice() will fail silently. I noticed this when I wrapped splice with another class I was working on, which should work according to the typings.While deleteCount may or may not be optional (your guess is as good as mine), it cannot be
undefined.⏯ Playground Link
I added several cases to test behaviour at the following:
Playground link with relevant code
💻 Code
class FailingWrapper{ arr = [1,2,3,4]; splice(start:number, deleteCount?: number){ // When deleteCount is 'undefined' method fails silently return this.arr.splice(start, deleteCount); } } const wrapper = new FailingWrapper(); const removed = wrapper.splice(2) // deleteCount is undefined console.log( `Case 4: Arr length is now ${wrapper.arr.length}, and removed length is ${removed.length}` );
🙁 Actual behavior
Case 4: Arr length is now 4, and removed length is 0splice() failed silently here because undefined was passed as deleteCount.
🙂 Expected behavior
Case 4: Arr length is now 2, and removed length is 2According to the typings splice() should have worked.
Addendum
It looks like the PR was reverted, but this edge case doesn't seem to be accounted for. Daniel Rosenwasser (@DanielRosenwasser) and Ryan Cavanaugh (@RyanCavanaugh) can you please take another look at this? It's easy enough to code around, but this is unexpected / undocumented behaviour, and was a serious pain to track down.
Reacted by Ryan CavanaughRyanCavanaugh commented
on Nov 19, 2021 MemberMore actionsThat's annoying as heck. We'll have to make it two overloads instead of having an optional parameter.
tjenkinson commented
on Nov 19, 2021 ContributorAuthorMore actionsRyan Cavanaugh (@RyanCavanaugh) how would that work? I just tried adding a
splice(start: number): T[];separate tosplice(start: number, deleteCount: number): T[];and it still allowsundefinedTo be clear, as noted above, when deleteCount is assigned undefined it defaults to 0. It's just weird because:
arr.splice(number) // deleteCount is length - start arr.splice(number, undefined) // deleteCount is 0
Behave totally different, when you'd expect they would be the same. I don't know if this should be handled by the transpiler, or simply just documented and warn users of weird behaviour of splice.
Even if it remains optional in ES5, could at least the documentation be clarified? The current documentation
@param deleteCountThe number of elements to remove. made me wonder what to expect. (Nothing is removed? One element is removed? All elements starting withstartare removed?) MDN documents the ES6 behavior, without mentioning that the ES5 spec said it to be required, and that ES5-compliant browsers practically had a different default value. But even if MDN was 100% correct, having to leave the IDE takes time.
TypeScript Version: 3.5.1
Search Terms: splice deleteCount
Code
Set the target to ES5 then
Expected behavior:
[1, 2, 3].splice(0);not allowedActual behavior:
[1, 2, 3].splice(0);allowedPlayground Link: this
deleteCountonly become optional in ES6ES5: https://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.12
ES6: https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.splice