055: Array Methods III: Reduce, Validation, and Sorting
Learning outcomes
By the end of this lesson, you can:
- trace an accumulator through
reduce()and choose a correct initial value; - use
some()andevery()for readable yes/no questions; - sort numbers and strings with a valid comparator;
- distinguish mutating
sort()from copyingtoSorted(); - prefer a clearer method or loop when
reduce()would hide intent.
Retrieval warm-up
- Which 043 method returns the first matching element?
- What does
findIndex()return when nothing matches? - Predict
[2, 5, 8].filter(number => number > 4).map(number => number * 2).
Answers: find(), -1, and [10, 16].
Vocabulary
- Accumulator: Running combined value carried through reduce calls. — Source: MDN: reduce
- Current value: Element supplied to the callback on this pass. — Source: MDN: reduce
- Initial value: Explicit starting accumulator; omitting it uses element 0. — Source: MDN: reduce
- Short-circuit: some/every stop iterating once the outcome is decided. — Source: MDN: every
- Comparator: (a,b)=>number defining sort order by sign. — Source: MDN: sort comparator
- In place: Mutation performed directly on the original array (sort/reverse/splice). — Source: MDN: sort
- Copying method: Non-mutating variant returning a fresh array (toSorted/toReversed/with). — Source: MDN: Array
- Reduce (official): "Array.prototype.reduce() executes a reducer function on each element, resulting in a single output value." — Source: MDN: Array.prototype.reduce()
- Comparator (official): "A comparator is a function that defines sort order by returning negative, zero, or positive." — Source: MDN: Array.prototype.sort()
Beginner mental model
reduce() is a running total board. Start with an explicit value, visit each item, return the board's next value, and finally receive the board. Although the final result may be any type, today use it where aggregation is natural: a number.
const quantities = [2, 1, 3];
const totalItems = quantities.reduce(
(runningTotal, quantity) => runningTotal + quantity,
0,
);
Trace it:
| Step | Accumulator | Current | Returned next accumulator |
|---|---|---|---|
| start | 0 | - | 0 |
| 1 | 0 | 2 | 2 |
| 2 | 2 | 1 | 3 |
| 3 | 3 | 3 | 6 |
The callback must return the next accumulator. Supply an initial value matching the result: 0 for a sum, 1 for a product when appropriate, "" for text, or [] for an array. Without one, an empty array throws TypeError, and the first element unexpectedly becomes the accumulator.
Do not make every operation a reduction. some() directly asks "does at least one match?" and returns early on the first truthy predicate. every() asks "do all match?" and returns early on the first falsy predicate. For an empty array, some() is false and every() is true; no item disproves the every-condition.
Sorting is different. sort() changes the source array and returns that same reference. Without a comparator it compares string forms, so [2, 100, 15].sort() becomes [100, 15, 2]. Numeric ascending uses (a, b) => a - b; descending uses (a, b) => b - a.
Modern toSorted() accepts the same comparator but returns a new, shallow array and leaves the original order alone. It has been broadly available since 2023. Use it when preserving source order matters.
Worked beginner example: cart checks and totals
const quantities = [3, 2, 1];
const lineTotals = [12, 32, 45];
const itemCount = quantities.reduce(
(count, quantity) => count + quantity,
0,
);
const subtotal = lineTotals.reduce(
(total, lineTotal) => total + lineTotal,
0,
);
const hasExpensiveLine = lineTotals.some((lineTotal) => lineTotal >= 40);
const quantitiesAreValid = quantities.every(
(quantity) => Number.isInteger(quantity) && quantity > 0,
);
const lowestLineFirst = lineTotals.toSorted((a, b) => a - b);
console.log(`Items: ${itemCount}`);
console.log(`Subtotal: $${subtotal}`);
console.log(`Expensive line: ${hasExpensiveLine}`);
console.log(`Valid quantities: ${quantitiesAreValid}`);
console.log(lowestLineFirst);
console.log(lineTotals);
Output:
Items: 6 Subtotal: $89 Expensive line: true Valid quantities: true [12, 32, 45] [12, 32, 45]
The two number lists happen to match because the source was already ascending. Reverse the source to make preservation visible. More importantly, lowestLineFirst !== lineTotals: toSorted() created a new outer array and did not reorder the source.
The subtotal trace is 0 + 12 = 12, then 12 + 32 = 44, then 44 + 45 = 89. Prepared line totals keep this lesson focused on aggregation; later object lessons can associate each total with a named product.
Sorting precisely
Comparator results mean:
- negative: place
abeforeb; - positive: place
aafterb; - zero or
NaN: treat their order as equal.
A comparator should be pure and consistent. Do not write (a, b) => a > b; that returns only booleans (0 or 1) and violates the expected negative/positive symmetry. For strings, use a.localeCompare(b), particularly when real user text may contain accents.
const original = [30, 4, 100];
const sameArray = original.sort((a, b) => a - b);
console.log(original); // [4, 30, 100]
console.log(sameArray === original); // true
const prices = [30, 4, 100];
const copied = prices.toSorted((a, b) => a - b);
console.log(prices); // [30, 4, 100]
console.log(copied); // [4, 30, 100]
Intermediate example: checkout summary
Keep each operation matched to its purpose instead of building one giant reducer:
function summarizeCart(quantities, lineTotals) {
const errors = quantities
.filter((quantity) => !Number.isInteger(quantity) || quantity < 1)
.map((quantity) => `Invalid quantity: ${quantity}`);
const subtotal = lineTotals.reduce((total, lineTotal) => total + lineTotal, 0);
const itemCount = quantities.reduce((count, quantity) => count + quantity, 0);
const canCheckout = quantities.length > 0 && errors.length === 0;
return [subtotal, itemCount, canCheckout, errors];
}
console.log(summarizeCart(quantities, lineTotals));
// [89, 6, true, []]
The returned positions are documented here as subtotal, item count, checkout permission, and errors. Named object fields become clearer after objects are taught, but are not required for this lesson. Why not force all four results through one reducer? It combines validation, arithmetic, and collection into a callback beginners must mentally simulate. Clear passes are usually preferable for normal cart sizes.
Optional advanced extension
Object comparators are an optional preview only. After object properties are introduced, sorting by stock and then name can look like this:
const previewCart = [
{ name: "Notebook", stock: 3 },
{ name: "Pen", stock: 3 },
];
const inventoryOrder = previewCart.toSorted(
(a, b) => a.stock - b.stock || a.name.localeCompare(b.name),
);
This small record shape and its property access are shown only to preview later lessons and are not required today. The || evaluates the name comparison only when stock comparison returns 0. ECMAScript sorting is stable, meaning comparator-equal items retain their earlier relative order, but an explicit second key communicates the intended order.
Common mistakes and debugging
- No initial value:
[].reduce(callback)throws. Add the correctly typed initial value. - No callback return: the next accumulator becomes
undefined, often producingNaN. - Wrong initial type:
"0"plus numbers concatenates strings. Start numeric totals at0. - Reducing a yes/no question: use
some()orevery()so evaluation can short-circuit. - Assuming
every()rejects empty arrays:[].every(...)istrue; separately requirevalues.length > 0when emptiness is invalid. - Default numeric sorting: provide a comparator.
- Unexpected source reorder: search for
.sort(and replace withtoSorted()when a copy is intended. - Invalid boolean comparator: return numeric differences or
localeCompare()results.
Debug a reducer by temporarily expanding it:
const subtotal = lineTotals.reduce((total, lineTotal) => {
console.log(total, lineTotal);
return total + lineTotal;
}, 0);
Best practices
- Give accumulators semantic names such as
totalorcount. - Provide an explicit initial value.
- Use
reduce()for genuine aggregation, not to prove cleverness. - Use
some()/every()for predicate questions andfilter()for collecting failures. - Prefer
toSorted()when original order is meaningful; document intentional in-placesort(). - Keep comparators pure, numeric, and consistent.
Checkpoint
Trace a three-number reduction on paper using columns for accumulator, current value, and returned value. Next, examine quantities.reduce((valid, quantity) => valid && quantity > 0, true) and rewrite it as the clearer quantities.every(...). Finish by comparing const sorted = values.sort(...) with const sorted = values.toSorted(...); predict both reference equality and source order. The explanation matters more than quickly producing syntax.
Exercises
Core
From quantities, calculate total units and determine whether any quantity is 3 or more.
const units = quantities.reduce((total, quantity) => total + quantity, 0);
const hasBulkLine = quantities.some((quantity) => quantity >= 3);
console.log(units, hasBulkLine);
Output: 6 true
Practice
Return lineTotals ordered from highest to lowest without changing the source.
const originalFirst = lineTotals[0];
const descendingTotals = lineTotals.toSorted((a, b) => b - a);
console.log(descendingTotals);
console.log(lineTotals[0] === originalFirst);
Output:
[45, 32, 12] true
Professional Extension
Write validateQuantities(quantities) returning an array whose first value is a Boolean validity result and whose second value is an array of messages for every invalid quantity. Validity must be false for an empty array.
function validateQuantities(quantities) {
const errors = quantities
.filter((quantity) => !Number.isInteger(quantity) || quantity < 1)
.map((quantity) => `${quantity} is an invalid quantity`);
return [quantities.length > 0 && errors.length === 0, errors];
}
console.log(validateQuantities([]));
console.log(validateQuantities([2, 0, 1.5]));
Output:
[false, []] [false, ["0 is an invalid quantity", "1.5 is an invalid quantity"]]
Recap
Describe the four reducer values on its first call when an initial value exists. Why does some() fit "at least one"? Why can every() be true for an empty array? What exactly does sort() mutate, and what kind of copy does toSorted() create?
