TechEarl

Modern JavaScript Array Methods: at, flat, toReversed, and groupBy

The array methods worth reaching for in modern JavaScript: at() for negative indexing, flat(Infinity), the copying toReversed/toSorted/toSpliced/with that fix the mutation trap, and the corrected grouping API (Object.groupBy, not the Array.prototype.group that never shipped).

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Modern JavaScript array methods reference: at() negative indexing, flat and flatMap, the copying toReversed/toSorted/toSpliced/with, and Object.groupBy with browser and Node support.

The short version: in 2026 you have negative indexing with at(), recursive flattening with flat(Infinity), a full family of copying methods (toReversed, toSorted, toSpliced, with) that finally let you reorder an array without mutating it, and grouping with Object.groupBy / Map.groupBy. That last one matters because a lot of older posts (mine included, once) tell you to use Array.prototype.group, which was renamed and never shipped. Here is what is real now, with the browser and Node floor for each.

at() for negative indexing

To read the last element of an array you used to write arr[arr.length - 1]. It works, but it is noisy and easy to fat-finger. at() takes a negative index and counts from the end:

javascript
const parts = ["a", "b", "c"];
parts.at(-1); // "c"
parts.at(-2); // "b"
parts[parts.length - 1]; // "c", the old way

Bracket access does not understand negative numbers (arr[-1] is just a property lookup that returns undefined), which is exactly the trap at() removes. It also works on strings and typed arrays, so "hello".at(-1) is "o". at() has been Baseline since 2022 (Chrome 92, Firefox 90, Safari 15.4, Node 16.6), so on any current runtime you can reach for it without a second thought.

flat() and flatMap()

flat() flattens nested arrays. The default depth is 1, so it only unwraps one level. For an arbitrarily deep structure, pass Infinity:

javascript
[1, [2, [3, [4]]]].flat();         // [1, 2, [3, [4]]]  (depth 1)
[1, [2, [3, [4]]]].flat(2);        // [1, 2, 3, [4]]
[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]

That flat(Infinity) line is the whole answer to "how do I recursively flatten a nested array in JavaScript." It supersedes the hand-rolled recursive helpers and library flatten functions that older code carried around.

flatMap() maps each element and then flattens the result by exactly one level. It is the idiomatic way to expand-or-drop items in one pass: return an array of several items to expand, or an empty array to drop:

javascript
const te_words = (s) => s.split(" ");
["the lazy dog", "fox"].flatMap(te_words);
// ["the", "lazy", "dog", "fox"]

// Drop odd numbers, double the even ones, in one pass:
[1, 2, 3, 4].flatMap((n) => (n % 2 ? [] : [n * 2]));
// [4, 8]

Both have been Baseline since 2020 (Node 11+), so they are safe everywhere.

The mutation trap: copying methods vs in-place methods

This is the part that bites people, so it gets the most space. reverse(), sort(), and splice() all mutate the array in place and return a reference to it (or, for splice, the removed elements). The return value lulls you into thinking you got a fresh array. You did not:

javascript
const scores = [3, 1, 2];
const reversed = scores.reverse();
// reversed is [2, 1, 3] ... and so is scores, now also [2, 1, 3].
// You just mutated the original while trying to make a copy.

If scores came in as a function argument, a prop, or a value someone else still holds a reference to, you have introduced a bug that shows up far from where you wrote it. The same trap applies to sort() (which also mutates) and splice().

The old fix was to copy first, then mutate the copy:

javascript
const reversed = [...scores].reverse(); // scores untouched
const sorted = [...scores].sort((a, b) => a - b);

Since ES2023 there is a cleaner fix: a copying counterpart for each mutating method. toReversed(), toSorted(), toSpliced(), and with() all leave the original alone and return a new array:

javascript
const scores = [3, 1, 2];

const reversed = scores.toReversed();          // [2, 1, 3]
const sorted   = scores.toSorted((a, b) => a - b); // [1, 2, 3]
const patched  = scores.with(0, 99);           // [99, 1, 2]  (replace index 0)
const spliced  = scores.toSpliced(1, 1);       // [3, 2]      (remove 1 at index 1)

scores; // [3, 1, 2] throughout. Nothing above touched it.

with(index, value) is the copying replacement for arr[index] = value: it returns a new array with one element swapped, which is exactly what you want in a React setState or any immutable update. toSpliced(start, deleteCount, ...items) mirrors splice() for inserting and removing.

The trade-off is honest: the copying methods allocate a new array every call, so in a tight loop over a huge array the in-place version is cheaper. For ordinary application code the readability and the absence of spooky-action-at-a-distance bugs win easily.

These four are ES2023, Baseline since 2023: Chrome 110, Firefox 115, Safari 16, Node 20+. If you ship to runtimes older than that (Node 18, old Safari), keep using the spread-then-mutate form or a polyfill.

Grouping: Object.groupBy and Map.groupBy (not Array.prototype.group)

Here is the correction. For years the grouping proposal was written up as Array.prototype.group and Array.prototype.groupToMap. Those names were withdrawn: a popular library had already monkey-patched Array.prototype with an incompatible group, so TC39 moved the feature off the prototype and onto static methods. Array.prototype.group and groupToMap never shipped to any stable browser. If a tutorial tells you to call arr.group(...), it is teaching an API that does not exist.

The real API is Object.groupBy(items, callbackFn):

javascript
const teams = [
  { name: "Arsenal", tier: "top" },
  { name: "Spurs", tier: "mid" },
  { name: "City", tier: "top" },
];

const byTier = Object.groupBy(teams, ({ tier }) => tier);
// {
//   top: [{ name: "Arsenal", ... }, { name: "City", ... }],
//   mid: [{ name: "Spurs", ... }],
// }

The callback returns the group key for each element; Object.groupBy collects the elements into arrays under those keys. One useful detail: the object it returns has a null prototype (Object.create(null)), so there is no risk of a group key like "constructor" or "__proto__" colliding with inherited properties.

Use Map.groupBy when your group key is not a string (an object, a number you want to keep as a number, anything). It returns a Map, and Map keys can be any value:

javascript
const odd = { label: "odd" };
const even = { label: "even" };
const byParity = Map.groupBy([1, 2, 3, 4], (n) => (n % 2 ? odd : even));
byParity.get(odd);  // [1, 3]
byParity.get(even); // [2, 4]

(Object.groupBy would coerce those object keys to the string "[object Object]" and merge them, which is why Map.groupBy exists.) Note that both are static methods. You call Object.groupBy(arr, fn), never arr.groupBy(fn).

Support is the newest thing on this page: Baseline since March 2024, Chrome/Edge 117, Firefox 119, Safari 17.4, Node 21+. On older runtimes the long-standing reduce-into-an-object pattern still does the job (see the reduce guide for that fold), and grouping is now the textbook case where Object.groupBy replaces a reduce you used to write by hand.

Support at a glance

MethodFirst BaselineNode floorMutates?
at()202216.6No
flat() / flatMap()202011No
toReversed() / toSorted() / toSpliced() / with()202320No (copying)
reverse() / sort() / splice()alwaysanyYes (in place)
Object.groupBy() / Map.groupBy()202421No

See also

Sources

Authoritative references this article was fact-checked against.

TagsJavaScriptarray methodstoReversedObject.groupByArray.prototype.atflatimmutable arraysES2023

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Run JavaScript When a CSS Animation or Transition Ends

Fire a JavaScript callback when a CSS transition or animation finishes, using the transitionend and animationend events. The propertyName filtering, the bubbling trap, the cases where the event never fires, and the modern Web Animations API promise alternative.