Skip to content
JavaScript js modern 4 min read

ES2023 & Beyond

ES2023 (the 14th edition of ECMAScript) is a smaller release than ES2020, but it lands two genuinely useful additions: searching arrays from the end with findLast/findLastIndex, and a family of non-mutating array methods — toSorted, toReversed, toSpliced, and with — that return a fresh copy instead of clobbering the original. It also formally standardized the #! hashbang for executable scripts. This page covers what shipped, then takes a peek at the high-profile proposals TC39 is still cooking.

findLast and findLastIndex

find and findIndex scan an array front to back. ES2023 adds their mirror images: findLast and findLastIndex walk from the end, returning the last element (or index) that satisfies the predicate. Before this, you had to reverse the array first — which mutated it or required an extra copy.

const transactions = [
  { id: 1, type: "credit" },
  { id: 2, type: "debit" },
  { id: 3, type: "credit" },
];

const lastCredit = transactions.findLast((t) => t.type === "credit");
const lastCreditIdx = transactions.findLastIndex((t) => t.type === "credit");

console.log(lastCredit.id, lastCreditIdx);

Output:

3 2

Both return undefined / -1 when nothing matches, mirroring their front-to-back counterparts exactly. They are available on Array and on typed arrays.

Change-array-by-copy: the immutable four

The headline feature is a set of methods that perform a common mutation but return a new array, leaving the receiver untouched. This makes them safe in React state updaters, Redux reducers, and anywhere shared data must not change in place.

Mutating (old)Copying (ES2023)What it does
arr.sort()arr.toSorted()Returns a sorted copy
arr.reverse()arr.toReversed()Returns a reversed copy
arr.splice(...)arr.toSpliced(...)Returns a copy with items removed/inserted
arr[i] = varr.with(i, v)Returns a copy with one index replaced
const scores = [40, 10, 30, 20];

const sorted = scores.toSorted((a, b) => a - b);
const reversed = scores.toReversed();
const replaced = scores.with(0, 99);
const spliced = scores.toSpliced(1, 1, 11, 12); // remove 1 at idx 1, insert 11,12

console.log(sorted);
console.log(reversed);
console.log(replaced);
console.log(spliced);
console.log(scores); // original is untouched

Output:

[ 10, 20, 30, 40 ]
[ 20, 30, 10, 40 ]
[ 99, 10, 30, 20 ]
[ 40, 11, 12, 30, 20 ]
[ 40, 10, 30, 20 ]

toSorted takes the same optional comparator as sort. with throws a RangeError for an out-of-bounds index (negative indices count from the end). These methods always produce a plain Array, even when called on a subclass.

Tip: Reach for these instead of the old [...arr].sort() / arr.slice().reverse() copy-then-mutate dance. They express intent in one call and avoid accidentally mutating shared state.

Hashbang grammar

ES2023 standardized the #! (hashbang or shebang) line that has long worked unofficially in Node and Deno. A file may begin with #! on its very first line, which the engine ignores, so the script can be made directly executable on Unix.

#!/usr/bin/env node
console.log("Runs directly as ./script.js after chmod +x");

The hashbang is only valid as the literal first characters of the file — not after a comment or whitespace.

A peek at what’s next

The following are TC39 proposals, not shipped standards. Stages run from 0 (idea) to 4 (ready to ship). Treat the syntax below as a preview; some details may still shift, and most require a transpiler or flag today.

Temporal

Temporal is a modern date/time API designed to replace the famously awkward Date. It offers immutable objects, explicit time zones, and a clean separation between instants, wall-clock times, and durations.

// Stage 3 proposal — Temporal
const meeting = Temporal.ZonedDateTime.from("2026-06-13T09:00[America/New_York]");
const inLondon = meeting.withTimeZone("Europe/London");

console.log(inLondon.toString());
// 2026-06-13T14:00:00+01:00[Europe/London]

Decorators

Decorators add metadata or wrap classes, methods, fields, and accessors with reusable behavior using @-prefixed syntax — popular in frameworks like Angular and NestJS.

function logged(value, { name, kind }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`calling ${name}`);
      return value.apply(this, args);
    };
  }
}

class Service {
  @logged
  fetch() { return "data"; }
}

Pipeline operator

The pipeline operator (|>) threads a value through a sequence of transformations left to right, replacing deeply nested calls with a readable flow.

// Proposal (Hack-style pipes)
const result = [1, 2, 3]
  |> %.map((n) => n * 2)
  |> %.filter((n) => n > 2);

Records & tuples

Records (#{ }) and tuples (#[ ]) are deeply immutable, compared by value rather than by reference — making #{ x: 1 } === #{ x: 1 } actually true.

// Proposal — deeply immutable, compared by value
const point = #{ x: 1, y: 2 };
console.log(point === #{ x: 1, y: 2 }); // true

Best Practices

  • Prefer findLast/findLastIndex over reversing an array just to search from the end — it’s clearer and avoids an extra copy.
  • Use toSorted, toReversed, toSpliced, and with for any array you don’t own or that’s shared (React/Redux state especially).
  • Keep the mutating originals (sort, reverse, splice) only for local arrays where in-place mutation is genuinely the goal and faster.
  • Remember with throws RangeError on out-of-bounds indices — validate the index when it comes from user input.
  • Don’t ship proposal syntax (Temporal, decorators, pipes, records) to production without a transpiler and a check of its current TC39 stage and browser/Node support.
  • Place a hashbang only as the literal first line of a file; anything before it makes it an ordinary comment-less syntax error.
Last updated June 1, 2026
Was this helpful?