JSON Array Methods JavaScript: map, filter, reduce & ES2023
Last updated:
JavaScript array methods transform JSON arrays without mutation — map(), filter(), reduce(), find(), and sort() cover 90% of JSON data processing tasks. map() runs in O(n) and always returns a new array of equal length; filter() returns a subset; reduce() accumulates to any value type — all three are pure functions that leave the original array unchanged. This guide covers the 12 most-used array methods with JSON examples, ES2023 additions (toSorted, toReversed, findLast), method chaining patterns, and performance comparisons. Every example uses typed JSON arrays with TypeScript generics.
map(), filter(), and reduce(): The Core JSON Array Methods
map(), filter(), and reduce() are the three foundational methods for processing JSON arrays. All three are pure — they do not modify the source array and always return a new value. They run in O(n) and accept a callback with (element, index, array) parameters. Together they handle the vast majority of JSON data transformation tasks: reshaping, filtering, and aggregating.
// TypeScript types for all examples
interface User {
id: number;
name: string;
age: number;
role: "admin" | "editor" | "viewer";
active: boolean;
score: number;
}
const users: User[] = [
{ id: 1, name: "Alice", age: 32, role: "admin", active: true, score: 92 },
{ id: 2, name: "Bob", age: 25, role: "editor", active: false, score: 78 },
{ id: 3, name: "Carol", age: 41, role: "viewer", active: true, score: 85 },
{ id: 4, name: "Dave", age: 19, role: "admin", active: true, score: 61 },
];
// ── map(): transform each element — always returns equal-length array ──
const names: string[] = users.map(u => u.name);
// ["Alice", "Bob", "Carol", "Dave"]
const summaries = users.map(u => ({
label: `${u.name} (${u.role})`,
isActive: u.active,
}));
// [{ label: "Alice (admin)", isActive: true }, ...]
// map() with index
const indexed = users.map((u, i) => ({ rank: i + 1, name: u.name }));
// [{ rank: 1, name: "Alice" }, ...]
// ── filter(): return subset matching predicate — O(n), always full scan ──
const activeUsers = users.filter(u => u.active);
// [Alice, Carol, Dave]
const highScorers = users.filter(u => u.score >= 85);
// [Alice, Carol]
// TypeScript type guard narrows return type
type AdminUser = User & { role: "admin" };
const admins = users.filter((u): u is AdminUser => u.role === "admin");
// admins is typed as AdminUser[]
// ── reduce(): accumulate to any output type ───────────────────────────
// Sum of scores
const totalScore = users.reduce((acc, u) => acc + u.score, 0);
// 316
// Build a lookup map by id — O(n) to build, O(1) per lookup
const byId = users.reduce<Record<number, User>>((acc, u) => {
acc[u.id] = u;
return acc;
}, {});
// { 1: Alice, 2: Bob, 3: Carol, 4: Dave }
// Group by role
const byRole = users.reduce<Record<string, User[]>>((acc, u) => {
(acc[u.role] ??= []).push(u);
return acc;
}, {});
// { admin: [Alice, Dave], editor: [Bob], viewer: [Carol] }
// Count active vs inactive
const counts = users.reduce(
(acc, u) => { acc[u.active ? "active" : "inactive"]++; return acc; },
{ active: 0, inactive: 0 }
);
// { active: 3, inactive: 1 }
// ── Performance characteristics ───────────────────────────────────────
// map() — O(n), always iterates full array, returns array of same length
// filter() — O(n), always iterates full array, returns subset array
// reduce() — O(n), always iterates full array, returns any value type
// All three: pure functions, no mutation of source arrayAlways provide the initial value (second argument) to reduce() — omitting it causes reduce() to use the first element as the accumulator and skip it during iteration. This silently breaks typed transformations (the accumulator is typed as User instead of your target type) and throws TypeError: Reduce of empty array with no initial value on empty arrays. The ??= (nullish assignment) operator in the groupBy pattern is cleaner than if (!acc[key]) acc[key] = [] — it initializes only when the property is null or undefined, not when it is 0 or an empty string.
find(), findIndex(), findLast(), and findLastIndex()
find() and findIndex() short-circuit — they stop iterating as soon as the predicate returns true. This makes them O(1) best case and O(n) worst case, significantly faster than filter()[0] for large arrays where the match is near the beginning. findLast() and findLastIndex() (ES2023) search from the end — optimal when the target element is more likely to be near the tail.
const users: User[] = [
{ id: 1, name: "Alice", age: 32, role: "admin", active: true, score: 92 },
{ id: 2, name: "Bob", age: 25, role: "editor", active: false, score: 78 },
{ id: 3, name: "Carol", age: 41, role: "viewer", active: true, score: 85 },
{ id: 4, name: "Dave", age: 19, role: "admin", active: true, score: 61 },
];
// ── find(): first match, short-circuits — returns element or undefined ──
const alice = users.find(u => u.id === 1);
// { id: 1, name: "Alice", ... }
const firstAdmin = users.find(u => u.role === "admin");
// { id: 1, name: "Alice", ... } — stops after first match
const missing = users.find(u => u.id === 999);
// undefined — always guard the result
if (missing) {
console.log(missing.name); // safe — TypeScript knows it is User here
}
// ── findIndex(): first matching index, -1 if not found ───────────────
const aliceIdx = users.findIndex(u => u.id === 1);
// 0
const notFound = users.findIndex(u => u.id === 999);
// -1
// findIndex is useful for in-place update (immutable pattern)
const idx = users.findIndex(u => u.id === 2);
if (idx !== -1) {
const updated = users.with(idx, { ...users[idx], active: true }); // ES2023
}
// ── findLast() and findLastIndex(): ES2023, search from end ───────────
const lastAdmin = users.findLast(u => u.role === "admin");
// { id: 4, name: "Dave", ... } — finds Dave, not Alice
const lastAdminIdx = users.findLastIndex(u => u.role === "admin");
// 3
// Use case: most recent log entry matching a condition
const logs = [
{ timestamp: "2024-01-01", level: "error", msg: "DB timeout" },
{ timestamp: "2024-01-02", level: "info", msg: "Restart" },
{ timestamp: "2024-01-03", level: "error", msg: "Connection refused" },
];
const latestError = logs.findLast(l => l.level === "error");
// { timestamp: "2024-01-03", level: "error", msg: "Connection refused" }
// ── find() vs filter(): when to use which ────────────────────────────
// find() — one result, short-circuits, returns element or undefined
// filter() — all results, full scan, returns array (possibly empty)
// Anti-pattern: filter()[0] wastes O(n) scanning the whole array
const bad = users.filter(u => u.id === 1)[0]; // scans all users
const good = users.find(u => u.id === 1); // stops at first match
// ── Polyfill for findLast/findLastIndex (older environments) ──────────
if (!Array.prototype.findLast) {
Object.defineProperty(Array.prototype, "findLast", {
value: function<T>(predicate: (el: T, i: number, arr: T[]) => boolean): T | undefined {
for (let i = this.length - 1; i >= 0; i--) {
if (predicate(this[i], i, this)) return this[i];
}
return undefined;
},
writable: true, configurable: true,
});
}The filter()[0] anti-pattern is one of the most common performance mistakes in JSON array processing — it always scans the entire array to collect all matches before returning just the first one. Replace it with find() which stops at the first match. For ID-based lookups in a hot path, consider building a Map or object index once (reduce() into a Record) and doing O(1) lookups instead of O(n) find() calls repeatedly on the same dataset. Browser support for findLast() and findLastIndex(): Chrome 97+, Firefox 104+, Safari 15.4+, Node.js 18+.
sort() vs toSorted(): Mutating vs Non-Mutating Sort
sort() is the most dangerous standard array method: it mutates the original array in place. When JSON data arrives from an API response and you need a sorted view, sort() permanently reorders the source — any other code holding a reference to the same array sees the sorted order. toSorted() (ES2023) returns a new sorted array, leaving the original intact. Use toSorted() by default; use sort() only when mutation is intentional and documented.
const users: User[] = [
{ id: 1, name: "Alice", age: 32, score: 92 },
{ id: 2, name: "Bob", age: 25, score: 78 },
{ id: 3, name: "Carol", age: 41, score: 85 },
{ id: 4, name: "Dave", age: 19, score: 61 },
];
// ── sort() — MUTATES original ─────────────────────────────────────────
users.sort((a, b) => a.age - b.age);
// users is now [Dave, Bob, Alice, Carol] — ORIGINAL IS CHANGED
// ── toSorted() — ES2023, returns new array, original unchanged ────────
const sortedByAge = users.toSorted((a, b) => a.age - b.age);
// sortedByAge: [Dave, Bob, Alice, Carol]
// users: still in original order — unchanged
// ── Numeric sort comparators ──────────────────────────────────────────
const byScoreAsc = users.toSorted((a, b) => a.score - b.score); // ascending
const byScoreDesc = users.toSorted((a, b) => b.score - a.score); // descending
// ── String sort with localeCompare ────────────────────────────────────
const byName = users.toSorted((a, b) => a.name.localeCompare(b.name));
// ── Multi-property sort: role first, then age ─────────────────────────
const byRoleThenAge = users.toSorted((a, b) =>
a.role.localeCompare(b.role) || a.age - b.age
);
// If role comparison returns 0 (equal), fall through to age comparison
// ── Default sort() without comparator — DANGER ───────────────────────
// sort() without comparator converts to strings and sorts lexicographically
[10, 9, 100].sort();
// [10, 100, 9] — WRONG for numbers! "100" < "10" < "9" lexicographically
// Always provide a numeric comparator for number arrays
[10, 9, 100].sort((a, b) => a - b);
// [9, 10, 100] — correct
// ── Sorting JSON from API — toSorted() prevents shared-state bugs ─────
async function fetchAndDisplay() {
const response = await fetch("/api/users");
const data: User[] = await response.json();
// WRONG: sort() mutates data — if anything else holds a ref, it sees sorted order
// const sorted = data.sort((a, b) => a.name.localeCompare(b.name)); // BUG
// CORRECT: toSorted() returns new array, data is untouched
const sorted = data.toSorted((a, b) => a.name.localeCompare(b.name));
return sorted;
}
// ── Polyfill for environments without toSorted() ──────────────────────
if (!Array.prototype.toSorted) {
Object.defineProperty(Array.prototype, "toSorted", {
value: function<T>(compareFn?: (a: T, b: T) => number): T[] {
return [...this].sort(compareFn);
},
writable: true, configurable: true,
});
}
// ── Stable sort guarantee (ES2019+) ──────────────────────────────────
// Both sort() and toSorted() are stable in V8 (Node 12+), SpiderMonkey,
// JavaScriptCore, and mandated by the spec since ES2019.
// Elements with equal comparator values retain their original relative order.
const items = [
{ name: "B", priority: 1 },
{ name: "A", priority: 1 },
{ name: "C", priority: 2 },
];
items.toSorted((a, b) => a.priority - b.priority);
// [{ name: "B" }, { name: "A" }, { name: "C" }] — B before A preserved (stable)The default sort() without a comparator converts elements to strings and sorts lexicographically — [10, 9, 100].sort() produces [10, 100, 9] because "100" < "10" < "9" lexicographically. Always provide a numeric comparator for number arrays: (a, b) => a - b. For a deeper dive into sort strategies for JSON objects, see the JSON array sorting guide.
flat() and flatMap() for Nested JSON Arrays
flat() flattens arrays of arrays; flatMap() combines map and one-level flat in a single pass. flatMap() is 20–60% faster than map().flat(1) because it avoids creating the intermediate mapped array. Both are essential when JSON responses contain nested array structures — tags arrays, line items, or paginated result sets concatenated from multiple API calls.
// ── flat(): flatten one level by default ─────────────────────────────
const nested = [[1, 2], [3, 4], [5, 6]];
nested.flat(); // [1, 2, 3, 4, 5, 6]
const deepNested = [1, [2, [3, [4]]]];
deepNested.flat(); // [1, 2, [3, [4]]] — one level
deepNested.flat(2); // [1, 2, 3, [4]] — two levels
deepNested.flat(Infinity); // [1, 2, 3, 4] — all levels
// ── flat() with JSON response containing nested arrays ────────────────
interface Category {
name: string;
tags: string[];
}
const categories: Category[] = [
{ name: "Tech", tags: ["javascript", "typescript", "node"] },
{ name: "Design", tags: ["css", "figma"] },
{ name: "DevOps", tags: ["docker", "kubernetes"] },
];
// Extract all tags — map() then flat()
const allTagsSlow = categories.map(c => c.tags).flat(1);
// ["javascript", "typescript", "node", "css", "figma", "docker", "kubernetes"]
// ── flatMap(): map + flat in one pass — 20-60% faster ─────────────────
const allTagsFast = categories.flatMap(c => c.tags);
// Same result, no intermediate array
// flatMap() with transformation
const tagLabels = categories.flatMap(c =>
c.tags.map(tag => `${c.name}: ${tag}`)
);
// ["Tech: javascript", "Tech: typescript", ..., "DevOps: kubernetes"]
// ── flatMap() as filter+map in one pass ───────────────────────────────
// Return empty array [] to skip elements (acts as filter)
interface Product {
name: string;
variants: { sku: string; price: number; inStock: boolean }[];
}
const products: Product[] = [
{ name: "Shirt", variants: [
{ sku: "S-SM", price: 25, inStock: true },
{ sku: "S-LG", price: 25, inStock: false },
]},
{ name: "Hat", variants: [
{ sku: "H-OS", price: 30, inStock: true },
]},
];
// Get all in-stock SKUs with their product name — filter + map in one pass
const inStockSkus = products.flatMap(p =>
p.variants
.filter(v => v.inStock)
.map(v => ({ product: p.name, sku: v.sku, price: v.price }))
);
// [
// { product: "Shirt", sku: "S-SM", price: 25 },
// { product: "Hat", sku: "H-OS", price: 30 },
// ]
// ── flatMap() for removing/expanding elements ─────────────────────────
// Remove null/undefined (return [] to skip)
const sparse = [1, null, 2, undefined, 3];
const dense = sparse.flatMap(x => x != null ? [x] : []);
// [1, 2, 3]
// Duplicate each element
const doubled = [1, 2, 3].flatMap(x => [x, x]);
// [1, 1, 2, 2, 3, 3]
// ── flat() for paginated API responses ───────────────────────────────
const pages = [
{ page: 1, results: [{ id: 1 }, { id: 2 }] },
{ page: 2, results: [{ id: 3 }, { id: 4 }] },
];
const allResults = pages.flatMap(p => p.results);
// [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]flatMap() only flattens one level deep — this is intentional. If the callback returns an array of arrays, only the outer array is flattened. For deeper flattening, combine map() with flat(depth). The flatMap(x => x != null ? [x] : []) idiom is the most elegant way to filter-and-unwrap in one pass — more readable than reduce() for simple cases. For JSON flatten (flattening nested objects to dot-notation keys rather than flattening arrays), see the dedicated guide.
some(), every(), and includes() for JSON Array Checks
some(), every(), and includes() are short-circuit boolean checks. All three stop iterating as soon as the result is determined — some() stops at the first match, every() stops at the first non-match, includes() stops at the first match. Use them for guard checks before processing and for validation of JSON payloads.
const users: User[] = [
{ id: 1, name: "Alice", role: "admin", active: true, score: 92 },
{ id: 2, name: "Bob", role: "editor", active: false, score: 78 },
{ id: 3, name: "Carol", role: "viewer", active: true, score: 85 },
];
// ── some(): true if at least one element matches — short-circuits ─────
const hasAdmin = users.some(u => u.role === "admin"); // true
const hasInactive = users.some(u => !u.active); // true
const hasPerfect = users.some(u => u.score === 100); // false
// Use some() as guard before expensive operations
if (!users.some(u => u.active)) {
throw new Error("No active users — cannot process");
}
// ── every(): true if ALL elements match — short-circuits on first false ─
const allActive = users.every(u => u.active); // false (Bob inactive)
const allScored = users.every(u => u.score >= 0); // true
const allHaveId = users.every(u => typeof u.id === "number"); // true — validation
// JSON payload validation
function isValidUserArray(data: unknown[]): data is User[] {
return data.every(
item =>
typeof item === "object" &&
item !== null &&
"id" in item &&
"name" in item &&
"role" in item
);
}
// ── includes(): fastest for primitive arrays — uses SameValueZero ─────
const roles = users.map(u => u.role);
// ["admin", "editor", "viewer"]
roles.includes("admin"); // true
roles.includes("owner"); // false
// includes() handles NaN correctly (indexOf() does not)
[NaN].includes(NaN); // true
[NaN].indexOf(NaN); // -1 — BUG: indexOf uses strict equality, misses NaN
// For object arrays, includes() checks reference equality — use some()
users.includes(users[0]); // true (same reference)
const clone = { ...users[0] };
users.includes(clone); // false — different reference, same values
users.some(u => u.id === users[0].id); // true — value-based check
// ── Combining for complex validation ─────────────────────────────────
const VALID_ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof VALID_ROLES[number];
function validateUsers(data: User[]): true {
if (!data.every(u => (VALID_ROLES as readonly string[]).includes(u.role))) {
throw new Error("Invalid role found");
}
if (!data.some(u => u.active)) {
throw new Error("At least one active user required");
}
return true;
}
// ── Performance: short-circuit comparison ────────────────────────────
// some() best case: O(1) — first element matches
// some() worst case: O(n) — no elements match (full scan)
// every() best case: O(1) — first element fails
// every() worst case: O(n) — all elements pass
// includes() — O(n) worst case, faster than some() for primitives
// due to no callback invocation overheadincludes() uses SameValueZero comparison — it correctly handles NaN (unlike indexOf() which uses strict equality and returns -1 for NaN): [NaN].includes(NaN) returns true; [NaN].indexOf(NaN) returns -1. For object arrays, never use includes() for value-based checks — it tests reference equality. Always use some() with a property comparator for object arrays. The every() method is ideal for runtime JSON schema validation without pulling in a schema library for simple cases.
Method Chaining: Combining Array Methods Efficiently
Method chaining applies multiple transformations in sequence — each method returns an array that feeds into the next. Standard chaining is readable and correct but creates an intermediate array after each step. For arrays under 10k items, this has negligible performance impact. For large arrays (100k+), a single-pass reduce() or flatMap()eliminates intermediate allocations and can be 2–5x faster.
const users: User[] = [
{ id: 1, name: "Alice", role: "admin", active: true, score: 92, age: 32 },
{ id: 2, name: "Bob", role: "editor", active: false, score: 78, age: 25 },
{ id: 3, name: "Carol", role: "viewer", active: true, score: 85, age: 41 },
{ id: 4, name: "Dave", role: "admin", active: true, score: 61, age: 19 },
{ id: 5, name: "Eve", role: "admin", active: true, score: 95, age: 28 },
];
// ── Standard chaining: readable, creates intermediate arrays ──────────
const topActiveAdminNames = users
.filter(u => u.active) // intermediate array 1: 4 elements
.filter(u => u.role === "admin") // intermediate array 2: 3 elements
.toSorted((a, b) => b.score - a.score) // intermediate array 3: sorted
.map(u => u.name); // final: ["Eve", "Alice", "Dave"]
// ── Optimization rule: combine multiple filter() calls into one ───────
const topActiveAdminNamesFast = users
.filter(u => u.active && u.role === "admin") // single O(n) pass
.toSorted((a, b) => b.score - a.score)
.map(u => u.name);
// Same result, one fewer intermediate array
// ── flatMap() replaces filter+map ────────────────────────────────────
// Pattern: flatMap(x => condition ? [transform(x)] : [])
const adminNames = users.flatMap(u =>
u.active && u.role === "admin" ? [u.name] : []
);
// ["Alice", "Dave", "Eve"] — filter + map in one pass, no intermediate array
// ── Single-pass reduce() for filter + transform + aggregate ──────────
const topAdminResult = users
.reduce<{ name: string; score: number }[]>((acc, u) => {
if (u.active && u.role === "admin") {
acc.push({ name: u.name, score: u.score });
}
return acc;
}, [])
.toSorted((a, b) => b.score - a.score)
.map(r => r.name);
// ["Eve", "Alice", "Dave"] — only one intermediate (sorted) array
// ── Chaining with error handling ─────────────────────────────────────
const safeSummary = users
.filter(u => u.score != null && u.name != null) // guard nulls first
.map(u => ({
display: `${u.name}: ${u.score}`,
badge: u.score >= 90 ? "gold" : u.score >= 75 ? "silver" : "bronze",
}))
.toSorted((a, b) => a.display.localeCompare(b.display));
// ── Rules for efficient chaining ─────────────────────────────────────
// 1. Put filter() before map() — reduce array size before transformation
// 2. Combine multiple filter() calls into one — single O(n) vs O(k*n)
// 3. Use flatMap() instead of map().flat(1) for shallow flattening
// 4. Use reduce() for combined filter+map+aggregate on large arrays
// 5. Put toSorted() last — sort is O(n log n), minimize what is sorted
// 6. Use find()/findIndex() instead of filter()[0] for single lookupsRule 1 is the most impactful: always filter before mapping. If 80% of elements are filtered out, the subsequent map() operates on 20% of the original data — reducing its cost by 80%. Rule 5 is equally important: sort is O(n log n), the most expensive standard array operation. Sort the smallest possible dataset by placing toSorted() after filter() and any other size-reducing operations. For the complete guide on JSON transform patterns, see the dedicated guide.
ES2023 and ES2024 New Array Methods
ES2023 introduced four non-mutating array methods and two new search methods. ES2024 added Object.groupBy() and Map.groupBy() as replacements for the common reduce()-to-group pattern. These additions complete the "copy" versions of all mutating methods, making immutable array processing the default idiomatic style in modern JavaScript.
// ── ES2023: toSorted() — non-mutating sort ───────────────────────────
const arr = [3, 1, 4, 1, 5, 9];
const sorted = arr.toSorted((a, b) => a - b); // [1, 1, 3, 4, 5, 9]
// arr is still [3, 1, 4, 1, 5, 9] — unchanged
// ── ES2023: toReversed() — non-mutating reverse ───────────────────────
const reversed = arr.toReversed(); // [9, 5, 1, 4, 1, 3]
// arr unchanged
// ── ES2023: toSpliced() — non-mutating splice ─────────────────────────
const original = [1, 2, 3, 4, 5];
const spliced = original.toSpliced(1, 2, 99); // remove 2 items at index 1, insert 99
// spliced: [1, 99, 4, 5]
// original: [1, 2, 3, 4, 5] — unchanged
// ── ES2023: with() — non-mutating index assignment ────────────────────
const withUpdated = original.with(2, 99); // set index 2 to 99
// withUpdated: [1, 2, 99, 4, 5]
// original unchanged
// Practical: immutable item update in React state
const users: User[] = [
{ id: 1, name: "Alice", active: true, score: 92 },
{ id: 2, name: "Bob", active: false, score: 78 },
];
const updatedUsers = users.with(
users.findIndex(u => u.id === 2),
{ ...users[1], active: true }
);
// updatedUsers: Alice (active), Bob (active: true)
// users: unchanged — React state update is safe
// ── ES2023: findLast() and findLastIndex() ────────────────────────────
const events = [
{ type: "login", user: "Alice", time: "09:00" },
{ type: "edit", user: "Bob", time: "09:15" },
{ type: "login", user: "Carol", time: "09:30" },
{ type: "logout", user: "Alice", time: "10:00" },
];
const lastLogin = events.findLast(e => e.type === "login");
// { type: "login", user: "Carol", time: "09:30" }
const lastLoginIdx = events.findLastIndex(e => e.type === "login");
// 2
// ── ES2024: Object.groupBy() — replaces reduce() for grouping ─────────
const grouped = Object.groupBy(users, u => u.active ? "active" : "inactive");
// { active: [Alice], inactive: [Bob] }
// groupBy with computed key
const byAgeGroup = Object.groupBy(users, u =>
u.score >= 90 ? "top" : u.score >= 75 ? "mid" : "low"
);
// { top: [Alice], mid: [Bob] }
// ── ES2024: Map.groupBy() — groupBy with Map (preserves insertion order) ─
const groupedMap = Map.groupBy(users, u => u.active);
// Map { true => [Alice], false => [Bob] }
groupedMap.get(true); // [Alice]
// ── Browser/Node.js support summary ──────────────────────────────────
// Method Chrome Firefox Safari Node.js
// toSorted() 110 115 16 20
// toReversed() 110 115 16 20
// toSpliced() 110 115 16 20
// with() 110 115 16 20
// findLast() 97 104 15.4 18
// findLastIndex() 97 104 15.4 18
// Object.groupBy() 117 119 17.4 21
// Map.groupBy() 117 119 17.4 21
// ── Polyfill toSorted and toReversed ─────────────────────────────────
if (!Array.prototype.toSorted) {
Object.defineProperty(Array.prototype, "toSorted", {
value: function<T>(fn?: (a: T, b: T) => number) { return ([...this] as T[]).sort(fn); },
writable: true, configurable: true,
});
}
if (!Array.prototype.toReversed) {
Object.defineProperty(Array.prototype, "toReversed", {
value: function<T>() { return ([...this] as T[]).reverse(); },
writable: true, configurable: true,
});
}The ES2023 "copy" methods (toSorted, toReversed, toSpliced, with) complete the push toward immutable array processing that map(), filter(), and reduce() established. Object.groupBy() (ES2024) is particularly impactful — it replaces the verbose and error-prone reduce()-to-group pattern with a clean, self-documenting one-liner. For the complete discussion of JSON groupBy patterns including groupBy with nested keys and multi-level grouping, see the dedicated guide.
Key Terms
- map()
Array.prototype.map(callback)appliescallback(element, index, array)to every element and returns a new array of the same length. It is a pure function — the source array is never modified. Time complexity: O(n). It always iterates the entire array (no short-circuit). The return type in TypeScript is inferred from the callback's return type:users.map(u => u.age)returnsnumber[]whenu.ageisnumber. Usemap()when every input element maps to exactly one output element. When elements need to be skipped or one input should produce multiple outputs, useflatMap()instead.- filter()
Array.prototype.filter(predicate)returns a new array containing only the elements for whichpredicate(element, index, array)returns truthy. The original array is unchanged. Time complexity: O(n) — always scans the full array regardless of how many elements match. In TypeScript, type guard predicates narrow the output type:.filter((u): u is AdminUser => u.role === "admin")returnsAdminUser[]. Combine multiple conditions in a singlefilter()call rather than chaining multiplefilter()calls — each chain step costs an additional O(n) pass. For finding a single element, preferfind()which short-circuits at the first match.- reduce()
Array.prototype.reduce(callback, initialValue)appliescallback(accumulator, element, index, array)to each element, threading the accumulator from one call to the next, and returns the final accumulator value. The output can be any type — number, string, object, array, or Map. Always provide theinitialValueargument: without it,reduce()uses the first element as the accumulator (changing behavior) and throws on empty arrays.reduce()runs in O(n) and is the most general array method —map()andfilter()can both be expressed asreduce()with different accumulators and callbacks.- find()
Array.prototype.find(predicate)returns the first element for which the predicate returns truthy, then immediately stops iterating. Returnsundefinedif no element matches. Time complexity: O(1) best case (first element matches), O(n) worst case (no match). Always guard the result:const user = arr.find(...); if (user) { /* use user */ }. Never usefilter()[0]when you need only one element —filter()scans the entire array and collects all matches before you discard all but the first.findLast()(ES2023) is the mirror that searches from the end.- flatMap()
Array.prototype.flatMap(callback)applies the callback to each element and flattens the result one level deep. Equivalent tomap(fn).flat(1)but implemented in a single pass — 20–60% faster for large arrays because no intermediate mapped array is created. Use cases: extracting nested arrays (posts.flatMap(p => p.tags)), filter+map in one pass (arr.flatMap(x => x.valid ? [x.value] : [])), and duplicating or splitting elements. Only flattens one level — for deeper flattening, usemap(fn).flat(depth)with an explicit depth argument.- toSorted()
Array.prototype.toSorted(comparator)is the ES2023 non-mutating version ofsort(). It returns a new sorted array — the original array is unchanged. Accepts the same comparator function assort(): a function returning a negative number, zero, or positive number to determine order. Bothsort()andtoSorted()are stable (preserve relative order of equal elements) per the ES2019+ specification. Supported in Chrome 110+, Firefox 115+, Safari 16+, Node.js 20+. For older environments, polyfill with:if (!Array.prototype.toSorted) { Array.prototype.toSorted = function(fn) { return [...this].sort(fn); }; }- method chaining
- Applying a sequence of array methods where each method's return value is the input for the next:
arr.filter(predicate).map(transform).toSorted(comparator). Each step in the chain returns a new array — the source and all intermediate arrays are immutable from the perspective of the chain. Standard chaining creates one intermediate array per step. Optimization strategies: putfilter()beforemap()to reduce array size early; combine multiple conditions into onefilter(); replacefilter().map()withflatMap(); and puttoSorted()last on the smallest possible array since sort is O(n log n). - pure function
- A function that always returns the same output for the same input and has no side effects — it does not modify external state, its arguments, or global variables.
map(),filter(),reduce(),find(),some(),every(),flat(),flatMap(),toSorted(),toReversed(), andincludes()are all pure — they never mutate the source array.sort(),reverse(),splice(),push(),pop(),shift(),unshift(), andfill()are impure — they mutate in place. Prefer pure methods for JSON data processing to prevent shared-state bugs, especially when the same array is referenced by multiple components or API response caches.
FAQ
What is the difference between map() and forEach() for JSON arrays?
map() returns a new array of equal length — one output element per input element — and can be chained: users.map(u => u.name).filter(Boolean). forEach() returns undefined and is used only for side effects (logging, mutating an external store). For JSON data processing, always prefer map() when you need a transformed array — using forEach() to build a new array by pushing into an external variable is an anti-pattern. Both iterate in O(n). Performance is nearly identical; the difference is semantic, not speed-based. In TypeScript, map() infers the return type from the callback: array.map(u => u.age) is typed as number[] when u.age is number, providing downstream type safety. forEach() cannot be part of a chain because it returns undefined — any attempt to call another method on its result throws TypeError: Cannot read properties of undefined.
How do I filter a JSON array by multiple conditions?
Combine conditions with && or || inside a single filter() callback: users.filter(u => u.active && u.age >= 18 && u.role === "admin"). A single filter() with combined conditions is more efficient than chaining multiple filter() calls — each chained call adds an O(n) pass. For complex or dynamic conditions, build a predicates array: const predicates = [u => u.active, u => u.age >= 18]; users.filter(u => predicates.every(p => p(u))). For TypeScript, use a type guard to narrow the return type: users.filter((u): u is AdminUser => u.role === "admin") returns AdminUser[] instead of User[]. When the conditions change at runtime (e.g., user-built filter UI), the predicates array pattern lets you add and remove conditions without changing the filter() call itself.
How do I use reduce() to transform a JSON array into an object?
Pass an empty object { } as the initial accumulator and build up properties in each iteration. ID lookup map: const byId = users.reduce<Record<number, User>>((acc, u) => { acc[u.id] = u; return acc; }, { }) — O(n) to build, O(1) per lookup. Group by property: const byRole = users.reduce((acc, u) => { (acc[u.role] ??= []).push(u); return acc; }, { }). Count occurrences: tags.reduce((acc, tag) => { acc[tag] = (acc[tag] ?? 0) + 1; return acc; }, { }). Always provide the initial value (second argument) — without it, reduce() uses the first element as the accumulator, which breaks typed transformations and throws on empty arrays. In ES2024, Object.groupBy(users, u => u.role) replaces the reduce-to-group pattern with a cleaner one-liner.
What is the difference between find() and filter() in JavaScript?
find() returns the first matching element (or undefined) and short-circuits — O(1) best case. filter() always scans the full array and returns all matching elements as a new array — always O(n). Use find() when uniqueness is guaranteed (by ID, by natural key) and you need exactly one item: const user = users.find(u => u.id === 42). Use filter() when you need all matches: const admins = users.filter(u => u.role === "admin"). The common anti-pattern users.filter(u => u.id === 42)[0] wastes O(n) to collect all matches (just one, since IDs are unique) before discarding the array. Always guard find() results since it can return undefined. findLast() (ES2023) searches from the end — use it when the target is more likely near the tail of the array (e.g., most recent event).
How do I sort a JSON array of objects by a property?
Use toSorted() (ES2023, non-mutating) or sort() (mutates in place) with a comparator function. Numeric ascending: users.toSorted((a, b) => a.age - b.age). Numeric descending: users.toSorted((a, b) => b.age - a.age). String: users.toSorted((a, b) => a.name.localeCompare(b.name)). Multi-property: users.toSorted((a, b) => a.role.localeCompare(b.role) || a.age - b.age) — the || falls through to the second comparator when the first returns 0 (equal). Without a comparator, sort() converts elements to strings and sorts lexicographically — [10, 9, 100].sort() produces [10, 100, 9], not [9, 10, 100]. Both sort() and toSorted() are stable (ES2019+). For more sort strategies, see the JSON array sorting guide.
What are the new ES2023 array methods toSorted() and toReversed()?
toSorted(comparator) returns a new sorted array — the original is unchanged. toReversed() returns a new reversed array — the original is unchanged. They are the non-mutating counterparts of sort() and reverse(), introduced in ES2023. Two more ES2023 additions complete the set: toSpliced(start, deleteCount, ...items) (non-mutating splice) and with(index, value) (non-mutating index assignment). findLast(predicate) and findLastIndex(predicate) (also ES2023) search from the end of the array. Browser support: Chrome 110+, Firefox 115+, Safari 16+, Node.js 20+ for the "to" methods; Chrome 97+, Firefox 104+, Node.js 18+ for findLast(). Polyfill: if (!Array.prototype.toSorted) { Array.prototype.toSorted = function(fn) { return [...this].sort(fn); }; }
How do I chain multiple array methods without creating intermediate arrays?
Standard chaining (filter().map().toSorted()) creates one intermediate array per step. For arrays under 10k items, this is negligible. For large datasets: (1) Replace filter().map() with flatMap(u => condition ? [transform(u)] : []) — one pass, no intermediate array. (2) Replace multi-step chains with a single reduce() that filters and transforms in one iteration. (3) In environments supporting Iterator Helpers (Chrome 122+, Node.js 22+), use array.values().filter(fn).map(fn).toArray() for lazy evaluation — elements are processed one at a time without materializing intermediate arrays. The most impactful optimization is ordering: always put filter() before map() to reduce dataset size before transformation, and put toSorted() last on the smallest possible array since sort is O(n log n).
How do I flatten a nested JSON array in JavaScript?
For arrays of arrays: [[1,2],[3,4]].flat() flattens one level; arr.flat(Infinity) flattens all nesting levels. For extracting nested arrays from objects (posts.flatMap(p => p.tags)), use flatMap() — it is 20–60% faster than map().flat(1) because it skips the intermediate mapped array. flatMap() only flattens one level deep; for deeper nesting, use map(fn).flat(depth) with an explicit depth. To flatten deeply nested structures of arbitrary depth: arr.flat(Infinity) or a recursive reduce(): function deepFlat(a) { return a.reduce((acc, v) => acc.concat(Array.isArray(v) ? deepFlat(v) : v), []); }. For flattening JSON objects (not arrays) to dot-notation keys, see the JSON flatten guide which covers recursive object flattening with the flat npm package.
Further reading and primary sources
- MDN: Array.prototype.map() — MDN reference for map() — return type inference, index parameter, and edge cases with sparse arrays
- MDN: Array.prototype.flatMap() — MDN reference for flatMap() — performance comparison with map().flat() and filter+map patterns
- TC39: Change Array by Copy Proposal (ES2023) — Original TC39 proposal that introduced toSorted(), toReversed(), toSpliced(), and with() to JavaScript
- MDN: Object.groupBy() — MDN reference for Object.groupBy() — ES2024 groupBy replacing reduce-to-object pattern
- V8 Blog: Array.prototype.sort stability — V8 blog post on the implementation of stable sort and TimSort in V8 — background on the ES2019 stability guarantee