Native JavaScript Utility Patterns
Quick Guide: Most utility-library calls now have a native equivalent with no bundle cost. The four that change how code is written are
structuredClonefor deep copies, the ES2023 immutable array methods (toSorted,toReversed,toSpliced,with),Object.groupBy/Map.groupBy, and the ES2025 Set operations. The two facts that catch people out: a structured clone drops prototypes and throws on functions, and the immutable array methods copy one level deep.
Detailed Resources:
- examples/core.md — cloning, immutable updates, negative indexing, searching from the end
- examples/arrays.md — deduplication, set operations, grouping, chunking, sorting, compacting
- examples/objects.md — typed pick/omit/mapValues, deep merge, lookup maps, freezing
- reference.md — lodash-to-native table, function utilities (debounce, throttle, memoize), availability
Before writing utility code
Reach for the native method before adding a dependency. The lodash-to-native table in reference.md settles most of it in one lookup, and the native call ships nothing.
Deep-copy with structuredClone. A JSON round trip turns a Date into a string and a Map or
Set into {}, and it does so silently — the failure surfaces later, at the first .getTime().
Use the to* array methods where the array is shared. sort, reverse and splice rewrite
the array in place, so a function that "returns a sorted copy" with sort has changed its caller's
data.
Convert to a Set before repeated membership checks. includes inside a filter is O(n²), and
the difference shows up at a few thousand elements.
Auto-detection: structuredClone, Object.groupBy, Map.groupBy, toSorted, toReversed, toSpliced, Array.prototype.with, findLast, findLastIndex, at(-1), Object.hasOwn, Object.fromEntries, Set.prototype.union, Set.prototype.intersection, Set.prototype.difference, symmetricDifference, isSubsetOf, isDisjointFrom, lodash alternative, native JavaScript utilities
Applies to:
- Array work — deduplicating, grouping, chunking, flattening, sorting immutably, searching from the end
- Object work — picking, omitting, merging, key/value transformation, lookup maps
- Set algebra — union, intersection, difference, subset tests
- Deep copying without a dependency
- Hand-rolled function utilities — debounce, throttle, memoize, once
Handled elsewhere:
- Date arithmetic, parsing and formatting —
Datealone does not cover it - Reactive updates — these methods produce new references, and what consumes a new reference is the concern of whatever owns the state
- Schema validation and runtime parsing — these utilities reshape data that is already trusted
Native or a dependency
Three cases, in order:
- A native method exists — use it. The table in reference.md is the lookup.
- No native method, but the implementation is ten lines — write it.
chunk,pick,omit,compact,deepMerge,debounce,throttle,memoizeandonceare all in this skill, written out and typed. - The edge cases are the feature — take the dependency. A debounce with
cancel,flushand leading/trailing edges; a deep merge with per-key array strategies; lazy evaluation over a dataset large enough that intermediate arrays matter. Reimplementing these badly costs more than the bytes.
Core patterns
Pattern 1: structuredClone for deep copies
Preserves Date, Map, Set, RegExp, typed arrays and circular references — everything a JSON
round trip destroys.
const snapshot = structuredClone(state);
// Preserved: Date, Map, Set, RegExp, ArrayBuffer, circular refs
// Throws on: functions, DOM nodes, Proxy, symbols
// Silently lost: the prototype — a class instance clones to a plain object
Full code: examples/core.md
Pattern 2: Immutable array methods (ES2023)
Four to* methods return a new array where the classic method rewrote the old one.
items.toSorted((a, b) => a.price - b.price); // vs .sort()
items.toReversed(); // vs .reverse()
items.toSpliced(index, 1); // vs .splice()
items.with(index, { ...items[index], done: true }); // vs items[i] = x
The copy is shallow: the new array holds the same element objects, which is why the update above spreads before writing.
Full code: examples/core.md
Pattern 3: Object.groupBy and Map.groupBy (ES2024)
const byStatus = Object.groupBy(orders, (order) => order.status);
// { pending: [...], shipped: [...] } — null-prototype, values are `T[] | undefined`
const byCustomer = Map.groupBy(orders, (order) => order.customer);
// Map keyed by the object itself — use this when the key is not a string
Object.groupBy coerces the key to a string and returns a null-prototype object, so reach for
Object.hasOwn(byStatus, key) rather than byStatus.hasOwnProperty(key).
Full code: examples/arrays.md
Pattern 4: Set operations (ES2025)
setA.union(setB);
setA.intersection(setB);
setA.difference(setB); // in A, not in B
setA.symmetricDifference(setB);
setA.isSubsetOf(setB);
setA.isSupersetOf(setB);
setA.isDisjointFrom(setB);
Each returns a Set, so spread when an array is wanted. The argument is any set-like with size,
has and keys — a Map qualifies, a plain array does not.
Full code: examples/arrays.md
Pattern 5: Searching and indexing from the end
logs.findLast((log) => log.level === "error"); // ES2023, single pass
logs.findLastIndex((log) => log.level === "error");
items.at(-1); // ES2022, undefined when empty
"file.backup.txt".split(".").at(-1); // "txt" — strings too
Full code: examples/core.md
Pattern 6: Object.fromEntries as the transformation pivot
entries → map/filter → fromEntries covers pick, omit, invert, mapValues, mapKeys and array-to-lookup
in one shape, so those helpers are four lines each rather than a dependency.
const keyed = Object.fromEntries(items.map((item) => [item.id, item]));
const withTax = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 1.1]),
);
const passing = Object.fromEntries(
Object.entries(scores).filter(([, score]) => score >= 90),
);
Full code: examples/objects.md
Red flags
Breaks at runtime:
structuredCloneon anything holding a function, a DOM node or a Proxy throwsDataCloneError— clone the data separately from the behaviour.JSON.parse(JSON.stringify(x))does not throw; it corrupts. ADatebecomes a string, aMaporSetbecomes{},undefinedmembers vanish, and the first method call on the result is where it surfaces.Object.groupByreturns a null-prototype object, soresult.hasOwnProperty(...)throws. UseObject.hasOwn.- Set methods reject a plain array — they need something with
size,hasandkeys.
Surprising behaviour:
structuredClonediscards the prototype. A class instance clones to a plain object with the same fields and none of the methods.toSorted()with no comparator sorts by string, so[10, 9, 1].toSorted()is[1, 10, 9]— exactly assort()always did.- The
to*methods copy one level. Editing an element of the new array edits the same object in the old one. arr.at(-1)types asT | undefinedwhatever the compiler options, so the empty case has to be handled.arr[arr.length - 1]types asTunlessnoUncheckedIndexedAccessis on — that is the form that lets anundefinedflow on unchecked.structuredCloneon anErrorsucceeds and keeps the standard fields, but acode, astatusor anything else attached to it is not in the copy.filter(Boolean)drops0,""andfalsealong with the nullish values, and TypeScript does not narrow the result —filter((x): x is T => x != null)does both jobs correctly.Object.freezeis shallow, and outside strict mode a write to a frozen object fails silently rather than throwing.Object.keysreturns integer-like keys first in ascending numeric order, whatever the insertion order — only string keys keep insertion order.