frontend / es features / 05_modern_collection_methods.md

Modern Array and Object Methods

5 min read source

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/reverse mutate — easy to forget when working with state. The immutable variants prevent surprise mutations.
  • structuredClone on class instances loses class identity — they become plain objects (no instanceof MyClass).
  • structuredClone doesn’t clone DOM — throws. Use library-specific tools for DOM.
  • Object.groupBy is null-prototype object — no inherited methods. Use Map.groupBy if you need methods.
  • Array.fromAsync collects all before resolving — for infinite/large sequences, use for await instead.
  • at(-0) — same as at(0). Don’t expect -0 to mean “last.”
  • TypeScript lib target 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 of arr[arr.length - 1]. findLast instead of [...arr].reverse().find().”
  • Object.hasOwn over hasOwnProperty — safer against shadowing and null-prototype objects.”
  • structuredClone for deep copy — handles Date, Map, Set, circular refs. Won’t clone functions or DOM.”
  • Object.groupBy/Map.groupBy replace lodash’s groupBy for most cases. Set operations (union, intersection) too.”
  • “Update tsconfig lib to the target ES version so these methods type-check; runtime support is broad.”

Cross-references

Further reading