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] = v | arr.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/findLastIndexover reversing an array just to search from the end — it’s clearer and avoids an extra copy. - Use
toSorted,toReversed,toSpliced, andwithfor 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
withthrowsRangeErroron 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.