Fix `.at`

Proposal: GitHub - tc39/proposal-relative-indexing-method: A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)

I know that the .at method is already in Stage 4 and it is not the right time to talk about its specification, but I decided to create this topic because its specification is very very harmful.

Take a look at the following code:

const arr = [1, 2, 3];
console.log(arr.at("foo")); // => 1
console.log(arr.at(NaN)); // => 1
console.log(arr.at(1.5)); // => 2

IMO, they should all return undefined!

The reason this happens is that the argument goes through ToIntegerOrInfinity. Instead, it should go through ToNumber and then validated with IsIntegralNumber.

I know that it has already been shipped to the browsers, but could you please consider fixing it once again?

related: Should it really be Math.trunc(n) || 0 · Issue #49 · tc39/proposal-relative-indexing-method · GitHub

11 Likes

This is how almost all functions in JavaScript behave. Compare the very similar arr.slice("foo") or arr.slice(1.5).

It's (in my opinion) unfortunate that JS is so aggressive about doing type coercion rather than rejecting unreasonable arguments, but it's a very, very strong precedent at this point, and it would probably be more confusing to have only some functions work that way.

2 Likes

JavaScript is already inconsistent. Whereas Array#indexOf uses ===, Array#includes uses SameValueZero. The new API has always prioritized ease of use.

[1, 2, NaN].indexOf(NaN); // => -1
[1, 2, NaN].includes(NaN); // => true

Stage 2 Change Array by Copy proposal has a simillar method .withAt, which uses IsIntegralNumber for now. The same should be true for .at.

https://tc39.es/proposal-change-array-by-copy/#sec-array.prototype.withAt

2 Likes

Includes doesn’t take an index. All index-taking functions coerce in the same way, just like .at

1 Like

.at should not be equated with Array#slice. Since arr.at(-1) is introduced as a syntax sugar for arr[arr.length - 1], it should be equated with general property access.

2 Likes

It looks like I won't be getting what I want at this point, so I created a module. I hope this will advance the discussion.

hash tag: #fix_ecmascript_at

It is not in fact a syntax sugar for arr[arr.length - 1] - [arr.at](http://arr.at)(n) is precisely syntax sugar for arr.slice(n)[0], and it behaves accordingly.

Luckily not, that would be bad.

[1].at(-5) !== [1].slice(-5)[0]

I understand what you are saying, but what JavaScript users want is a syntax sugar for arr[n >= 0 ? n : n + arr.length], not a syntax sugar for arr.slice(n)[0].

If you don't want to change it, please enlighten us that arr.at(n) is not a syntax sugar of what we want. I don't think many people really understand what that is.

2 Likes

I wrote a Japanese article about Array#at, but no one is saying it is what we want it to be.

To pick up on the responses to this article:

Oh no, it's not too late, we should take it back. TypeScript's number type can't determine NaN.
いやあ今なら間に合うから取り消すべきだよ/typescriptのnumber型はNanを判定できないと思う・・・ - Fushihara のブックマーク / はてなブックマーク

This is a shame :rage: I'm not sure I'd recommend it.
【B!】【追記あり】ES2022 Array#at がちょっとおかしい - Qiita / これは残念仕様😡 手放しに推奨するのは微妙 - yasu-log のブックマーク / はてなブックマーク

I guess we shouldn't use Array#at.
Array#atは使わないほうが良さそう - door-s-dev のブックマーク / はてなブックマーク

You can also see other responses from the following site, in Japanese.

The difference is only observable if you're passing something which is not an integral number. But if you've ended up with a non-integral number as an argument to .at, you almost certainly have a bug anyway.

This behavior is also the ecosystem precedent. Compare, for example, lodash's nth method, which works exactly the same way. I don't think this has ever actually caused problems in practice. (E.g., there don't seem to be any issues on the lodash repo expressing surprise at this behavior.)

1 Like

I don't know how many people have used _.nth, but I suspect it's a small percentage of JavaScript users. I've used lodash, but I didn't know that method existed.

Since there are users who are currently unhappy with Array#at, could you please think about making it more usable, instead of getting hung up on consistency for no reason?

1 Like

I'm sure there's a bias and I don't think it's enough people answering, but don't you think it's weird that the current spec makes misleading to such a large percentage of people?

3 Likes

OK. I've thought about it a lot, and it seems to be correct that a argument of a method named at should be rounded to an integer with ToIntegerOrInfinity, just like the similarly named String#charAt and String#charCodeAt. Indeed, it makes sense.

Syntax sugar for property access should be proposed in other ways.

1 Like

Since all of javascript numbers are floats, then they would be susceptible to rounding errors, so in some ways, it can actually be a good thing that it rounds.

const index = 0.3 + 0.3 + 0.3 + 0.1 // 0.99999
[1, 2, 3].at(index) // 1

I also agree that it should behave more like the slice() function. Yes, at() mimics array indexing in some ways, but it's also a function, not an operator, and so I would want it to be consistent with other functions.

In the end, we can disagree on this, but like has been previously pointed out, if you're trying to index with NaN, 'hello world', etc, then you've already got a bug in your program that needs to be sorted out. Fixing this type coercion issue with just a single function still leaves all of the other built in function susceptible to this kind of type coercion none-sense. The only real way to defend against it is to use something like typescript, not to try and change how the language operators after X point in time.

It looks plausible. However, in that code example, people might expect it to return 2 instead of 1. (Not that it matters, but the semicolon-less code apparently doesn't work as expected).

IMO, returning a seemingly correct value makes it harder to find bugs than returning nothing at all. And since it is impossible for TypeScript to determine whether a number is NaN or not, we have no way to fight it. In order to avoid creating bugs as much as possible, we should not rely on unstable methods such as Array#at. We should explicitly round numbers to integer values for property access.

const arr = [1, 2, 3];
const index = 0.3 + 0.3 + 0.3 + 0.1; // 0.99999

console.log(arr[Math.trunc(index)]); // 1
console.log(arr[Math.round(index)]); // 2

Whoops, you're right, it truncates, not rounds. I guess that's a pretty bad argument then :)

1 Like

Pardon me for jumping in, but a week has passed. Is there any progress?

What's being asked for here is a fundamental change to a stage 4 proposal that's already being shipped. The ship has already sailed. It's too late to fix this, even if we wanted to (which, not everyone seems to want this changed). People are already writing code that depends on this proposal.

The proposal is stage 4 and shipping in multiple browsers, there’s no changes that are likely to be made here.