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.
Interview Q&A
Q: Immutable array methods — show me.
A: ES2023 added non-mutating versions of sort, reverse, splice, and element replacement:
const arr = [3, 1, 2];
// Mutating (old)
arr.sort(); // mutates arr in place, returns the same array
arr.reverse();
arr.splice(1, 1, 99);
// Immutable (new)
const sorted = arr.toSorted((a, b) => a - b); // arr unchanged
const reversed = arr.toReversed();
const spliced = arr.toSpliced(1, 1, 99);
const replaced = arr.with(0, 99); // replace index 0 with 99 → new array
These are the cleanest fix for the classic React bug:
// Bug — sort mutates state
const [items, setItems] = useState([3, 1, 2]);
items.sort(); // mutates! React may not re-render
setItems(items);
// Fix
setItems(items.toSorted());
For state libraries (Redux, Zustand), reducers must be immutable — toSorted removes the need for [...items].sort() workarounds.
Q: at() — what’s it for?
A: 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].
Q: findLast / findLastIndex.
A: 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"); // 2
Replaces the slow [...arr].reverse().find(...) pattern.
Q: Object.groupBy and Map.groupBy (ES2024).
A:
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.
Q: Object.hasOwn — why over hasOwnProperty?
A: 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" };
obj1.hasOwnProperty("hasOwnProperty"); // TypeError — calls a string
const obj2 = Object.create(null);
obj2.foo = 1;
obj2.hasOwnProperty("foo"); // TypeError — no prototype
// Safe — works on both
Object.hasOwn(obj1, "hasOwnProperty"); // true
Object.hasOwn(obj2, "foo"); // true
Always prefer Object.hasOwn for own-property checks.
Q: structuredClone — proper deep clone.
A: 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);
console.log(original.nested.items); // [1, 2, 3] — unaffected
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).
Q: Array.fromAsync (ES2024).
A: 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.
Q: flat and flatMap.
A: 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"].flatMap(s => s.split(" ")); // ["a", "b", "c", "d"]
flatMap is map(fn).flat(1) — common pattern: split each item into multiple.
Q: Map/Set extensions.
A: 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.
Q: Map.prototype.emplace — proposal, useful pattern.
A: 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.
Q: String modern methods.
A: 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
- “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: ../04_typescript/09_strictness_flags.md - Iterators and async iteration: 04_iterators_and_generators.md
- Immutable updates in state libraries: ../07_state_managers/
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