Modern Array and Object Methods
The post-ES2020 additions to Array, Object, and Map that quietly simplify code. The biggest wins: immutable array methods (toSorted, toReversed, toSpliced, with — ES2023) that return a new array instead of mutating; at() for negative-index access; findLast/findLastIndex; Object.hasOwn (safer hasOwnProperty); Object.groupBy/Map.groupBy (ES2024); structuredClone for deep copy. Most senior wins from this list are about not mutating and not reaching for lodash when a native method exists.
Immutable array methods — show me.
ES2023 added non-mutating versions of sort, reverse, splice, and element replacement:
const arr = [3, 1, 2];
// Mutating (old)
// mutates arr in place, returns the same array
arr.sort();
arr.reverse();
arr.splice(1, 1, 99);
// Immutable (new)
// arr unchanged
const sorted = arr.toSorted((a, b) => a - b);
const reversed = arr.toReversed();
const spliced = arr.toSpliced(1, 1, 99);
// replace index 0 with 99 → new array
const replaced = arr.with(0, 99);These are the cleanest fix for the classic React bug:
// Bug — sort mutates state
const [items, setItems] = useState([3, 1, 2]);
// mutates! React may not re-render
items.sort();
setItems(items);
// Fix
setItems(items.toSorted());For state libraries (Redux, Zustand), reducers must be immutable — toSorted removes the need for [...items].sort() workarounds.
at() — what’s it for?
Negative-index access without arr.length - n:
const arr = [10, 20, 30];
arr.at(0); // 10
arr.at(-1); // 30 (last)
arr.at(-2); // 20
arr[-1]; // undefined — bracket access doesn't accept negative
"hello".at(-1); // "o"Available on Array, String, TypedArrays. Cleaner than arr[arr.length - 1].
findLast / findLastIndex.
ES2023 — like find / findIndex but search from the end:
const events = [
{ id: 1, type: "login" },
{ id: 2, type: "click" },
{ id: 3, type: "click" },
];
events.findLast(e => e.type === "click"); // { id: 3, type: "click" }
events.findLastIndex(e => e.type === "click"); // 2Replaces the slow [...arr].reverse().find(...) pattern.
Object.groupBy and Map.groupBy (ES2024).
const items = [
{ name: "apple", category: "fruit" },
{ name: "carrot", category: "vegetable" },
{ name: "banana", category: "fruit" },
];
Object.groupBy(items, (item) => item.category);
// {
// fruit: [{ name: "apple", ... }, { name: "banana", ... }],
// vegetable: [{ name: "carrot", ... }]
// }
Map.groupBy(items, (item) => item.category);
// Map { "fruit" => [...], "vegetable" => [...] }Map.groupBy allows non-string keys (object keys preserve identity). Replaces _.groupBy from lodash for most cases.
Object.hasOwn — why over hasOwnProperty?
Object.prototype.hasOwnProperty can be shadowed (a property named hasOwnProperty on the object) or be inaccessible on objects created with Object.create(null) (no prototype):
const obj1 = { hasOwnProperty: "foo" };
// TypeError — calls a string
obj1.hasOwnProperty("hasOwnProperty");
const obj2 = Object.create(null);
obj2.foo = 1;
// TypeError — no prototype
obj2.hasOwnProperty("foo");
// Safe — works on both
Object.hasOwn(obj1, "hasOwnProperty"); // true
Object.hasOwn(obj2, "foo"); // trueAlways prefer Object.hasOwn for own-property checks.
structuredClone — proper deep clone.
Built-in deep clone using the structured clone algorithm:
const original = {
name: "Ada",
date: new Date(),
nested: { items: [1, 2, 3] },
map: new Map([["a", 1]]),
set: new Set([1, 2]),
};
const copy = structuredClone(original);
copy.nested.items.push(99);
// [1, 2, 3] — unaffected
console.log(original.nested.items);Handles:
- Plain objects, arrays.
Date,RegExp.Map,Set.ArrayBuffer, typed arrays.- Circular references.
null/ primitives.
Does NOT handle:
- Functions (throws
DataCloneError). - DOM nodes (throws).
- Class instances lose their prototype (become plain objects).
For most app data, replaces JSON.parse(JSON.stringify(x)) (which loses Date, Map, Set, fails on circular refs).
Array.fromAsync (ES2024).
Collects an async iterable into an array:
async function* fetchItems() {
for (let i = 0; i < 3; i++) {
yield await fetch(`/api/items/${i}`).then(r => r.json());
}
}
const items = await Array.fromAsync(fetchItems());
// Waits for each, collects.Equivalent of for await + push, but declarative. Useful with paginated APIs returning async generators.
flat and flatMap.
Older (ES2019) but underused:
[[1, 2], [3, 4]].flat(); // [1, 2, 3, 4]
[[1, [2]], [3]].flat(); // [1, [2], 3] — depth 1
[[1, [2]], [3]].flat(2); // [1, 2, 3]
[[1, 2], [3, 4]].flat(Infinity); // fully flatten
// ["a", "b", "c", "d"]
["a b", "c d"].flatMap(s => s.split(" "));flatMap is map(fn).flat(1) — common pattern: split each item into multiple.
Map/Set extensions.
ES2025 set operations:
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.union(b); // {1,2,3,4}
a.intersection(b); // {2,3}
a.difference(b); // {1}
a.symmetricDifference(b); // {1,4}
a.isSubsetOf(b); // false
a.isSupersetOf(b);
a.isDisjointFrom(b);Replaces the [...a].filter(x => b.has(x)) pattern. Modern Chrome/Edge/Safari/Firefox have it.
Map.prototype.emplace — proposal, useful pattern.
Stage 2 proposal for “get-or-insert”:
// Today
const map = new Map<string, number[]>();
function push(k: string, v: number) {
if (!map.has(k)) map.set(k, []);
map.get(k)!.push(v);
}
// With emplace (proposed)
map.emplace(k, {
insert: () => [],
update: (existing) => { existing.push(v); return existing; },
});Not yet stable; mention if you want to flex modern proposal awareness.
String modern methods.
Useful additions:
"foo".at(-1); // "o"
"abc abc abc".replaceAll("a", "X"); // "Xbc Xbc Xbc"
"abc".padStart(5, "0"); // "00abc"
"abc".padEnd(5, "0"); // "abc00"
" abc ".trimStart(); // "abc "
" abc ".trimEnd();
"abc".repeat(3); // "abcabcabc"replaceAll is the big modern win — replaces the awkward /g-regex pattern for the common case.
Gotchas
- Old
sort/reversemutate — easy to forget when working with state. The immutable variants prevent surprise mutations. structuredCloneon class instances loses class identity — they become plain objects (noinstanceof MyClass).structuredClonedoesn’t clone DOM — throws. Use library-specific tools for DOM.Object.groupByis null-prototype object — no inherited methods. UseMap.groupByif you need methods.Array.fromAsynccollects all before resolving — for infinite/large sequences, usefor awaitinstead.at(-0)— same asat(0). Don’t expect-0to mean “last.”- TypeScript
libtarget must include the right ES version for these methods to type-check."lib": ["ES2024", "DOM"]for the recent ones.
Related
Further reading
- MDN — Array: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
- MDN —
Object.groupBy: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy - MDN —
structuredClone: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone - TC39 proposals dashboard: https://github.com/tc39/proposals
Interview angle 5
- “How do you sort an array without mutating it?” -
toSorted(), and likewisetoReversed(),toSpliced()andwith(i, v)(ES2023). They replace the[...arr].sort()dance and matter most in state management, where an accidental in-placesortmutates the state object you were about to compare. - “What replaced the awkward index arithmetic?” -
at(-1)for the last element instead ofarr[arr.length - 1], andfindLast/findLastIndexinstead of reversing a copy to search from the end. - “Why prefer
Object.hasOwnoverhasOwnProperty?” - becauseobj.hasOwnPropertybreaks on an object with anullprototype and can be shadowed by a property of that name.Object.hasOwn(obj, key)is a static call that neither problem reaches. - “How do you deep-clone an object?” -
structuredClone, which handlesDate,Map,Set, typed arrays and circular references. It cannot clone functions, DOM nodes or class identity, so it throws rather than silently producing something wrong. - “What can you drop lodash for now?” -
Object.groupByandMap.groupBycover mostgroupByuse, andSetgainedunion,intersectionanddifference. The remaining reasons to keep it are the deep-object helpers.- Follow-up: “Why does TypeScript say these do not exist?” - the
libsetting intsconfigis behind the runtime. Runtime support is broad; you have to raiselibto the matching ES version for them to type-check.
- Follow-up: “Why does TypeScript say these do not exist?” - the