Array.prototype.idx method, alternative to Array.prototype.at

@ljharb @theScottyJam I understand the current coercion logic and agree with how they coerce the values.

I read through the entire fix-at thread before creating this new topic. I am not advocating for changing the current coercion method. This new .idx() method would not stand to change the ways things are currently done but would offer new functionality that is currently untapped by any method.

The fix-at thread argues that .at(-1) should coerce differently but produce the same value it currently does (in most cases).

This is entirely different in that the value in this method is different from any current approach to array element retrieval by indexes. Currently, there is no "true method" method for retrieving array elements by index other than bracket notation which only works for positive integers.

.at() is close but does not work for negative index values. Instead, negative values work more similarly to the arr[arr.length - n] approach (actually arr.slice(n)[0] under the hood).

.idx() would fix the issue specifically of accepting any index value, including negative index values, on a zero-index basis. Without a method like this, implementations looking for elements from the end of the array will always have to account for an offset of -1 only from the right-side, creating an imbalance in indexing. This is how you would have to set up a function now to account for that offset:

function getIndexByOffsetDirection(arr, offset = 0, direction = 1) {
    if (
        offset == null ||
        isNaN(offset) ||
        !isFinite(offset) ||
        offset % 1 !== 0 ||
        offset < 0
    ) {
        throw new Error('The offset argument must be a positive integer >= 0')
    }
    if (![-1, 1].includes(direction)) {
        throw new Error('The direction argument must be either -1 or 1')
    }
    return arr.at(direction === 1 ? offset : offset * -1 - 1)
}

getIndexByOffsetDirection(["a", "b", "c"], 0, 1) // -> "a"
getIndexByOffsetDirection(["a", "b", "c"], 1, 1) // -> "b"
getIndexByOffsetDirection(["a", "b", "c"], 2, 1) // -> "c"
getIndexByOffsetDirection(["a", "b", "c"], 0, -1) // -> "c"
getIndexByOffsetDirection(["a", "b", "c"], 1, -1) // -> "b"
getIndexByOffsetDirection(["a", "b", "c"], 2, -1) // -> "a"

This expression arr.at(direction === 1 ? offset : offset * -1 - 1) is fully acceptable from the perspective of the typical array coercion but as the only currently available method for retrieving an array element by indexing, is also awfully limiting by requiring the developer to account for the -1 offset in negative-index cases.

Of course, the above function would likely never need to be used if there were a method available such as .idx() as .idx() would simply take in any positive or negative integer representation of the desired index, including -0 and return that element.

That is the main difference between the use-case of **.at()** and .idx(). The **.at()** method is not meant for retrieving array elements by index explicitly, but rather retrieving array elements by the traditional coercion method. This is highly valuable as well, and I would not want to change the current function of .at(), but I do see a separate and solid case to be made for .idx() for explicit retrieval of array elements by index, instead of the usual coercion.

Using .idx() instead of .at(), that line in the above function could be simplified to arr.idx(offset * direction). This would even account for cases of -0 as 0 * -1 === -0 is true is JavaScript.

With a method like .idx(), however, a function like the one above would like not even be needed, as the entire function could simply accept any positive or negative integer, like this:

["a", "b", "c"].idx(0) // -> "a"
["a", "b", "c"].idx(1) // -> "b"
["a", "b", "c"].idx(2) // -> "c"
["a", "b", "c"].idx(-0) // -> "c"
["a", "b", "c"].idx(-1) // -> "b"
["a", "b", "c"].idx(-2) // -> "a"

@bergus I do understand your concern, as many developers have not worked with -0 before, but I'd make the case that technology is constantly evolving and new methods are continually being introduced which require/prompt a developer to expand their knowledge. Another point here is that while it may seem unintuitive, -0 is actually more "true" or intuitive from an index perspective. While many developers may be ignorant/unaware of -0 as I was for many years, the truth is that -0 does exist in JavaScript as a generally equal but separate entity from its positive counterpart 0. If anything, introducing a new method like this would expand the community's awareness of -0 and create a very practical use-case for -0 where currently there are not many.


Also in response to @ljharb's concern about the "very un-JS-like strict argument type checks", I 100% agree and would not advocate for implementing the Array.prototype.idx in this fashion. My example was merely a pseudo code representation of how this code could function in use.

The primary use-case of and justification for this Array.prototype.idx would be array element retrieval by +/- index value. If the primary concern is that this deviates widely from the traditional coercion method, then I have no issue working this around the usual coercion method. Array.prototype.idx would even act as a decorator or wrapper to the Array.prototype.at method like this:

Array.prototype.idx = function(index) {
    return this.at(index < 0 || Object.is(index, -0) ? index - 1 : index)
}

["a", "b", "c"].idx(0) // -> "a"
["a", "b", "c"].idx(1) // -> "b"
["a", "b", "c"].idx(2) // -> "c"
["a", "b", "c"].idx(-0) // -> "c"
["a", "b", "c"].idx(-1) // -> "b"
["a", "b", "c"].idx(-2) // -> "a"
["a", "b", "c"].idx("foo") // -> "a"
["a", "b", "c"].idx(NaN) // -> "a"
["a", "b", "c"].idx(1.5) // -> "b"

This would work with the same coercion method traditionally used by JavaScript but with a very slim wrapper to account for the -1 offset. This is fairly common practice and would be extremely useful to save developers who have a use-case where they need to offset the -1 in some cases.