Skip to content
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

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

Open
tjenkinson opened this issue Jul 31, 2019 · 15 comments · Fixed by #32643
Open

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

tjenkinson opened this issue Jul 31, 2019 · 15 comments · Fixed by #32643
Labels
Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript
Milestone

Comments

@tjenkinson
Copy link
Contributor

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 31, 2019

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

@RyanCavanaugh
Copy link
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.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 31, 2019
@tjenkinson
Copy link
Contributor Author

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.

@fatcerberus
Copy link

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.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 31, 2019

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

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Jul 31, 2019
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jul 31, 2019
@RyanCavanaugh RyanCavanaugh added the Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript label Jul 31, 2019
@RyanCavanaugh
Copy link
Member

OK, that sounds pretty annoying. Accepting PRs

@RyanCavanaugh
Copy link
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.

@tjenkinson
Copy link
Contributor Author

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jul 31, 2019

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

tjenkinson added a commit to tjenkinson/TypeScript that referenced this issue Jul 31, 2019
In ES5 `deleteCount` is not an optional argument. If it is not provided it defaults to 0 as a side effect of `undefined` being converted to an integer.

In ES6 `deleleteCount` is optional, and it defaults to the length of the array minus the start index.

If you are targeting ES5 but don't provide `deleteCount` the behaviour will be different depending on the environment your build is running in.

fixes microsoft#32638
tjenkinson added a commit to tjenkinson/TypeScript that referenced this issue Jul 31, 2019
In ES5 `deleteCount` is not an optional argument. If it is not provided it defaults to 0 as a side effect of `undefined` being converted to an integer.

In ES6 `deleleteCount` is optional, and it defaults to the length of the array minus the start index.

If you are targeting ES5 but don't provide `deleteCount` the behaviour will be different depending on the environment your build is running in.

fixes microsoft#32638
@tjenkinson
Copy link
Contributor Author

PR: #32643

sandersn pushed a commit that referenced this issue Mar 12, 2020
* make splice `deleteCount` required in es5.d.ts

In ES5 `deleteCount` is not an optional argument. If it is not provided it defaults to 0 as a side effect of `undefined` being converted to an integer.

In ES6 `deleleteCount` is optional, and it defaults to the length of the array minus the start index.

If you are targeting ES5 but don't provide `deleteCount` the behaviour will be different depending on the environment your build is running in.

fixes #32638

* update baselines
@buck-silver
Copy link

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. @DanielRosenwasser and @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.

@RyanCavanaugh RyanCavanaugh reopened this Nov 19, 2021
@RyanCavanaugh
Copy link
Member

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

@tjenkinson
Copy link
Contributor Author

@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

@buck-silver
Copy link

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.

@jtotht
Copy link

jtotht commented Jun 10, 2023

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
Labels
Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants