Lodash _.sumBy() method
What you’ll learn
- How
_.sumByis implemented as(array && array.length) ? baseSum(array, baseIteratee(iteratee, 2)) : 0—and whatbaseIterateeaccepts. - All five iteratee shapes: function, property path string, dotted path,
[key, value]pair, and matches object. - Why missing properties are silently skipped (no
NaN)—and how that differs from_.meanBy. - How the string-concatenation trap from
_.sumresurfaces when an iteratee returns a string. - The all-or-nothing BigInt rule, the friendly empty-input guard, and how booleans coerce when the iteratee is a predicate.
Prerequisites
Read _.sum() first—_.sumBy shares its baseSum backbone and inherits every quirk. _.meanBy() is the sibling to compare against for missing-field semantics.
- Iteratee shorthand: any
*Byhelper accepts a function, a string property path, a dotted path, a[key, value]tuple, or a matches object.baseIterateepicks the right shape. - JavaScript
+coercion: a single string in the running total flips arithmetic to concatenation. Same trap as_.sum.
Overview
Two-liner: guard the array, then call baseSum(array, baseIteratee(iteratee, 2)). baseIteratee turns whatever you pass into a function—'total' becomes _.property('total'), 'price.base' becomes a nested getter, ['active', true] becomes _.matchesProperty, and a plain object becomes _.matches. baseSum then folds with native +, skipping any element whose iteratee returns undefined. That last detail is why missing properties produce a clean (if smaller) total instead of NaN.
Five iteratee shapes
Function, property path, dotted path, [k,v] tuple, matches object.
Missing ⇒ skipped
No NaN from absent properties—the row just doesn’t contribute.
String poisoning
If your iteratee returns a string, the whole sum becomes a concatenated string.
Syntax
_.sumBy(array, [iteratee=_.identity]) - array (Array): the array to iterate over.
- iteratee (Function | string | Array | Object): the iteratee invoked per element. Default
_.identity. - Returns (number): the sum.
0for empty,null, orundefinedarray. May be a string (concat) if any iteratee return is a string. May beNaNif any iteratee return isNaN.
All five iteratee shapes
The docs example plus the four shorthand forms. They’re interchangeable: pick whichever is clearest at the call site.
import sumBy from "lodash/sumBy";
const objects = [{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }];
// (a) function iteratee -- docs example
console.log(sumBy(objects, o => o.n)); // 20
// (b) property-path string shorthand
console.log(sumBy(objects, "n")); // 20
// (c) dotted path for nested objects
const carts = [
{ line: { price: 10 } },
{ line: { price: 25 } },
{ line: { price: 30 } }
];
console.log(sumBy(carts, "line.price")); // 65
// (d) [key, value] -- matchesProperty (returns boolean -> coerces 1/0)
const tasks = [
{ active: true, n: 1 },
{ active: false, n: 2 },
{ active: true, n: 3 }
];
console.log(sumBy(tasks, ["active", true])); // 2 (counts matches)
// (e) matches object -- same idea, slightly nicer for multi-key
console.log(sumBy(tasks, { active: true })); // 2
// No iteratee (or null) -- falls back to _.identity, behaves like _.sum
console.log(sumBy([1, 2, 3])); // 6
console.log(sumBy([1, 2, 3], null)); // 6 Missing fields, null, NaN — what actually happens
A common bit of folklore says “_.sumBy returns NaN when a property is missing.” That’s wrong: baseSum skips elements whose iteratee returns undefined. Below shows the actual rules and how to recover the “missing-as-zero” behaviour if you want it.
import sumBy from "lodash/sumBy";
const invoices = [
{ id: 1, total: 100 },
{ id: 2 }, // total is missing -> undefined -> SKIPPED
{ id: 3, total: 50 }
];
console.log(sumBy(invoices, "total")); // 150 (not NaN)
// null in a property coerces to 0 (counted)
console.log(sumBy([{ n: 1 }, { n: null }, { n: 3 }], "n")); // 4
// NaN poisons the entire result
console.log(sumBy([{ n: 1 }, { n: NaN }, { n: 3 }], "n")); // NaN
// Want missing-as-zero? Use a function iteratee with ?? (or || for 0/null/undefined)
console.log(sumBy(invoices, r => r.total ?? 0)); // 150 -- same as skipping here
// Different result if you also want zeros from null:
const sparse = [
{ n: 1 },
{ n: null },
{}
];
console.log(sumBy(sparse, "n")); // 1 (null -> 0 counted, undefined skipped)
console.log(sumBy(sparse, r => r.n ?? 0)); // 1
console.log(sumBy(sparse, r => r.n || 0)); // 1
// Friendly guard for missing arrays
console.log(sumBy([], "n")); // 0
console.log(sumBy(null, "n")); // 0
console.log(sumBy(undefined, "n")); // 0 Realistic patterns — revenue, string-trap, BigInt
Three production-y cases: a derived-field rollup (qty × price), the string-trap you’ll hit with untyped data, and how BigInt behaves.
import sumBy from "lodash/sumBy";
// Revenue rollup: derive a value per row
const sales = [
{ product: "phone", revenue: 800, unitsSold: 2 },
{ product: "tablet", revenue: 500, unitsSold: 1 },
{ product: "laptop", revenue: 1200, unitsSold: 3 }
];
console.log(sumBy(sales, o => o.revenue * o.unitsSold)); // 5700
// String-trap: untyped data (e.g. parsed from a CSV) keeps prices as strings
const items = [
{ name: "A", price: "5" },
{ name: "B", price: "10" },
{ name: "C", price: "7" }
];
console.log(sumBy(items, "price")); // "5107" (a string!)
console.log(typeof sumBy(items, "price")); // "string"
// Fix: coerce inside the iteratee
console.log(sumBy(items, o => Number(o.price))); // 22
// BigInt: all-or-nothing
console.log(sumBy([{ n: 1n }, { n: 2n }, { n: 3n }], "n")); // 6n
try {
sumBy([{ n: 1n }, { n: 2 }], "n");
} catch (err) {
console.log("Threw:", err.message); // "Cannot mix BigInt and other types..."
}
// Array-likes (anything with a length) work too
const arrLike = { length: 3, 0: { n: 10 }, 1: { n: 20 }, 2: { n: 30 } };
console.log(sumBy(arrLike, "n")); // 60 📋 _.sumBy vs _.sum, _.meanBy, and reduce
| Scenario | _.sumBy | _.sum | _.meanBy | Notes |
|---|---|---|---|---|
| Array of objects, sum a key | _.sumBy(rows, "n") | requires rows.map(r => r.n) | _.meanBy(rows, "n") | _.sumBy avoids the intermediate array. |
| Missing field on a row | skipped, no NaN | n/a (uses raw values) | skipped from sum but counted in divisor (silently lowers mean) | The most important difference between _.sumBy and _.meanBy. |
Empty / null array | 0 | 0 | NaN | Only _.meanBy lacks the friendly guard (because of the divisor). |
| Iteratee returns a string | concat → string | concat → string | concat → string / NaN | Same root cause: native + coercion. |
| BigInt in every row | 6n | 6n | throws (BigInt ÷ Number) | _.meanBy can’t divide a BigInt by a Number. |
| Predicate iteratee (count matches) | _.sumBy(rows, ["k", v]) works (true→1) | n/a | works but rarely meaningful | Booleans coerce to 1 / 0. |
Pitfalls to avoid
“Missing returns NaN” is wrong
Several copies of the legacy reference claim missing properties produce NaN. They don’t—baseSum’s explicit undefined guard skips them. If you actually want NaN on missing data, use _.sumBy(rows, r => r.n) with r.n undefined deliberately converted to NaN (e.g. +r.n).
Untyped numeric data poisons the sum
CSV/JSON parsing often leaves numbers as strings. _.sumBy(items, "price") over {price: "5"} rows returns a concatenated string. Use a function iteratee with Number() to be safe.
Falls back to identity
_.sumBy(objects) with no iteratee behaves like _.sum(objects)—adding raw objects with + gives the useless string "[object Object][object Object]". Always pass an iteratee for object arrays.
All-or-nothing
Every row must be a BigInt, or the iteratee must coerce to BigInt. Mixing throws.
[key, value] sums booleans
_.sumBy(rows, ["active", true]) counts matching rows (true→1, false→0). To sum a numeric field on matching rows, filter first: _.sumBy(rows.filter({active: true}), "price").
❓ FAQ
Summary
- Purpose: sum each element’s iteratee value—
baseSum(array, baseIteratee(iteratee, 2))with the same empty-array guard as_.sum. - Remember: five iteratee shapes; missing → skipped (no
NaN);null→0; one stringy return poisons to concat;NaNpoisons; BigInt all-or-nothing; empty/null array →0. - Next: back to Lodash Math methods, or read the official Lodash docs for _.sumBy.
_.sumBy([{price:1}, {}, {price:2}], 'price') returns 3, not NaN. baseSum skips any element whose iteratee returns undefined—and because there’s no divisor (unlike _.meanBy), missing fields are silently dropped without biasing the result. The flip side: a single stringy property value flips + to concatenation, so _.sumBy([{x:'5'}, {x:'10'}], 'x') returns the string "510".
6 people found this page helpful
