Modern Array and Object Methods
TL;DR
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.
In depth
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 / edge cases
- 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.
What a senior is expected to say 6
- “Immutable array methods (
toSorted,toReversed,with) — ES2023. Replaces the[...arr].sort()pattern; cleaner in state-management code.” - “
at(-1)instead ofarr[arr.length - 1].findLastinstead of[...arr].reverse().find().” - “
Object.hasOwnoverhasOwnProperty— safer against shadowing and null-prototype objects.” - “
structuredClonefor deep copy — handles Date, Map, Set, circular refs. Won’t clone functions or DOM.” - “
Object.groupBy/Map.groupByreplace lodash’sgroupByfor most cases. Set operations (union,intersection) too.” - “Update
tsconfiglibto the target ES version so these methods type-check; runtime support is broad.”
Cross-references
- TypeScript
libandtarget: tsconfig Strictness Flags — What Each One Prevents - Iterators and async iteration: Iterators and Generators (incl. for-await-of)
- Immutable updates in state libraries: State Management — Senior Interview Prep
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