Skip to content

deleteCount param to splice() is not optional in es5 types #32638

Description

TypeScript Version: 3.5.1

Search Terms: splice deleteCount

Code
Set the target to ES5 then

[1, 2, 3].splice(0); // should error. deleteCount missing
[1, 2, 3].splice(0, 3); // ok

Expected behavior:
[1, 2, 3].splice(0); not allowed

Actual behavior:
[1, 2, 3].splice(0); allowed

Playground Link: this

deleteCount only become optional in ES6

ES5: 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

Activity

  1. AnyhowStep commented on Jul 31, 2019

    @AnyhowStep
    Contributor

    ES5 Array.prototype.splice (start, deleteCount, ...

    1. Let actualDeleteCount be min(max(ToInteger(deleteCount),0), len – actualStart).

    ToInteger

    1. Let number be the result of calling ToNumber on the input argument.

    ToNumber

    Argument Type Result
    Undefined NaN

    ToInteger

    1. If number is NaN, return +0.

    max(ToInteger(undefined),0) is just max(+0, 0) is just zero

  2. RyanCavanaugh commented on Jul 31, 2019

    @RyanCavanaugh
    Member

    Unless 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) gives 0 which is a good default argument in many cases.

    For example Number.prototype.toFixed lists its argument as required, but TypeScript doesn't enforce this because (0).toFixed() is a well-formed and predictable result. Same for String.prototype.slice - its arguments are both listed as "required" but has predictable and expected behavior with one.

  3. tjenkinson commented on Jul 31, 2019

    @tjenkinson
    ContributorAuthor

    Hmm 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 deleteCount unless you know for sure what environment you are running in.

  4. fatcerberus commented on Jul 31, 2019

    @fatcerberus

    The wording used in the spec is interesting:

    When the splice method 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.

  5. AnyhowStep commented on Jul 31, 2019

    @AnyhowStep
    Contributor

    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,

    1. 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

  6. added
    BugA bug in TypeScript
    and removed
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    on Jul 31, 2019
  7. RyanCavanaugh commented on Jul 31, 2019

    @RyanCavanaugh
    Member

    OK, that sounds pretty annoying. Accepting PRs

  8. RyanCavanaugh commented on Jul 31, 2019

    @RyanCavanaugh
    Member

    Thanks 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.

  9. tjenkinson commented on Jul 31, 2019

    @tjenkinson
    ContributorAuthor

    Awesome. 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 :)

  10. AnyhowStep commented on Jul 31, 2019

    @AnyhowStep
    Contributor

    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 actualStart is computed,

    1. Let relativeStart be ToInteger(start).
    2. ReturnIfAbrupt(relativeStart).
    3. If relativeStart < 0, let actualStart be max((len + relativeStart),0); else let actualStart be min(relativeStart, len).
  11. added 2 commits that reference this issue on Jul 31, 2019
    af0fe7d
    368d007
  12. tjenkinson commented on Jul 31, 2019

    @tjenkinson
    ContributorAuthor

    PR: #32643

  13. added a commit that references this issue on Mar 12, 2020
    ddcf139
  14. buck-silver commented on Nov 19, 2021

    @buck-silver

    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 undefined as 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 0

    splice() failed silently here because undefined was passed as deleteCount.

    🙂 Expected behavior

    Case 4: Arr length is now 2, and removed length is 2

    According 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.

  15. RyanCavanaugh commented on Nov 19, 2021

    @RyanCavanaugh
    Member

    That's annoying as heck. We'll have to make it two overloads instead of having an optional parameter.

  16. tjenkinson commented on Nov 19, 2021

    @tjenkinson
    ContributorAuthor

    Ryan Cavanaugh (@RyanCavanaugh) how would that work? I just tried adding a splice(start: number): T[]; separate to splice(start: number, deleteCount: number): T[]; and it still allows undefined

  17. buck-silver commented on Nov 19, 2021

    @buck-silver

    To 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.

  18. jtotht commented on Jun 10, 2023

    @jtotht

    Even if it remains optional in ES5, could at least the documentation be clarified? The current documentation @param deleteCount The number of elements to remove. made me wonder what to expect. (Nothing is removed? One element is removed? All elements starting with start are 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDomain: lib.d.tsThe issue relates to the different libraries shipped with TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions