Implement countBy(collection, iteratee) — walk an array and tally how many elements fall into each bucket, where the bucket for an element is decided by iteratee. Think of sorting a deck of cards into piles by suit and then reporting "13 hearts, 13 spades, …", except the rule for which pile a card belongs to is something you pass in. This mirrors Lodash's _.countBy: the iteratee can be a function you call on each element, or a property-name string as shorthand for "group by this field."
// collection: T[] — the array to tally.
// iteratee: ((el: T) => K) — a function returning the bucket key, OR
// | string — a property name; shorthand for el => el[name].
// returns: Record<string, number>
// An object mapping each produced key (coerced to a string) to the
// COUNT of elements that produced it.
function countBy(collection, iteratee): Record<string, number>;
// Function iteratee: bucket floats by their integer part.
countBy([6.1, 4.2, 6.3], Math.floor);
// → { '6': 2, '4': 1 }
// Function iteratee: split into even and odd piles.
countBy([1, 2, 3, 4, 5], (n) => (n % 2 === 0 ? 'even' : 'odd'));
// → { odd: 3, even: 2 }
// Property-name iteratee: group people by their city field.
const people = [
{ name: 'Ada', city: 'London' },
{ name: 'Linus', city: 'Helsinki' },
{ name: 'Grace', city: 'London' },
];
countBy(people, 'city');
// → { London: 2, Helsinki: 1 }
countBy(items, 'city') behaves like countBy(items, (x) => x.city).6 and a key of '6' land in the same bucket; the returned object's keys are always strings.groupBy).countBy([], fn) returns {}.{} already responds to result['toString'] and result['constructor'] — make sure a bucket named "toString" counts like any other key rather than colliding with an inherited method.You'll walk an array once and tally how many elements fall into each bucket, where the bucket for an element is whatever key the iteratee produces.
Imagine a pile of receipts and you want to know how many came from each store. You go through them one at a time, and for each receipt you read the store name and add one to that store's tally. countBy is exactly that loop, with one twist: how you read the bucket off each element is a parameter. Sometimes the bucket is the result of a function — Math.floor(price), order.status === 'paid'. Sometimes it's just a field on the element, and the caller passes the field name as a string: countBy(receipts, 'store'). The output is an object whose keys are the buckets and whose values are the counts.
Hold three things in your head: the element you're looking at, the key the iteratee derives from it, and the counts object you're filling in. The whole algorithm is a single pass: for each element, compute its key, and bump that key's entry by one. Nothing is sorted, nothing is collected — you only ever store a running integer per key. The two iteratee forms (a function vs. a property-name string) are not two algorithms; they're two ways to get the key. Normalize them into a single "give me the key for this element" function up front, and the rest of the code never has to care which form the caller used.
The instinct is right — loop, derive a key, increment — but the first version usually leaves out the second iteratee form and reaches for a plain {}:
function countBy(collection, iteratee) {
const counts = {};
for (const element of collection) {
const key = iteratee(element); // assumes iteratee is always a function
if (counts[key]) {
counts[key] = counts[key] + 1;
} else {
counts[key] = 1;
}
}
return counts;
}
This handles the function case and the happy path, but it has two real bugs. First, it throws the moment someone calls countBy(people, 'city') — a string isn't callable, so iteratee(element) blows up with "iteratee is not a function." Second, the if (counts[key]) guard and the plain {} both stumble on keys that clash with inherited properties. With counts = {}, the expression counts['toString'] is the inherited toString function, which is truthy — so the code takes the + 1 branch and computes [Function] + 1, which is NaN. A bucket named "toString" silently corrupts.
function countBy(collection, iteratee) {
// Normalize the two iteratee forms into a single getter. A function is used
// directly; a string `name` becomes "read element[name]". From here down,
// the loop only ever calls getKey — it never re-checks the iteratee's type.
const getKey =
typeof iteratee === 'function'
? iteratee
: (element) => element[iteratee];
// Object.create(null) makes a bucket map with NO prototype, so keys like
// "toString" or "constructor" behave like any other key instead of colliding
// with inherited methods.
const counts = Object.create(null);
for (const element of collection) {
// String(...) makes the coercion explicit: a key of 6 and a key of "6"
// are the same bucket. (Object keys coerce to strings anyway; this just
// makes the intent obvious and keeps the ?? lookup honest.)
const key = String(getKey(element));
// `?? 0` treats a never-seen key as count 0, so the first sighting writes
// 1 and every repeat climbs from there.
counts[key] = (counts[key] ?? 0) + 1;
}
return counts;
}
module.exports = { countBy };
Three changes carry the fix. The getKey line collapses "function or string" into one getter, so the hot loop is type-agnostic. Object.create(null) removes the inherited-property landmine — there is no toString on the prototype to collide with. And (counts[key] ?? 0) + 1 replaces the brittle truthiness check: ?? only falls back when the left side is null or undefined, so a legitimately stored 0 would still be respected (counts never store 0, but the operator is the correct tool regardless).
Trace countBy([6.1, 4.2, 6.3], Math.floor) end to end.
iteratee is Math.floor, a function, so getKey is Math.floor. counts starts as an empty null-prototype object.
counts = {} (no prototype)
element 6.1 → key = String(Math.floor(6.1)) = '6'
→ counts['6'] = (undefined ?? 0) + 1 = 1
→ counts = { '6': 1 }
element 4.2 → key = String(Math.floor(4.2)) = '4'
→ counts['4'] = (undefined ?? 0) + 1 = 1
→ counts = { '6': 1, '4': 1 }
element 6.3 → key = String(Math.floor(6.3)) = '6'
→ counts['6'] = (1 ?? 0) + 1 = 2
→ counts = { '6': 2, '4': 1 }
return { '6': 2, '4': 1 }
The third element is the interesting one: '6' already exists with value 1, so 1 ?? 0 is 1 and the count climbs to 2. The keys come out in first-seen order — '6' then '4' — because that's the order JavaScript preserves for string keys on an object.
countBy(people, 'city') passes a string. If you write iteratee(element) unconditionally, V8 throws "iteratee is not a function" the instant a caller uses the property-name form. Normalize first: const getKey = typeof iteratee === 'function' ? iteratee : (el) => el[iteratee];.{} for the counts. const counts = {} inherits toString, constructor, hasOwnProperty, and more. Counting a value of "toString" reads the inherited function: counts['toString'] is truthy and [Function] + 1 is NaN, so that bucket is silently wrong. Use Object.create(null) (or guard every lookup with Object.prototype.hasOwnProperty.call(counts, key)), so inherited names can't shadow real buckets.if (counts[key]) instead of ?? 0. Truthiness is the wrong test. It happens to work for positive counts, but it's exactly what makes the inherited-toString bug fire (the inherited function is truthy). Reach for (counts[key] ?? 0) + 1 so the "first time" case is decided by presence, not by whether the current value is truthy.[1, 2, 1] by identity and bucketing ['1', '2', '1'] by identity produce the same object — { '1': 2, '2': 1 } — because object keys are always strings. If a caller expects numeric keys back, that's not possible with a plain object; they'd need a Map. Make the coercion explicit with String(...) so this isn't a surprise.element[iteratee] or calling iteratee(element) is fine — those don't change anything. But don't be tempted to sort or splice collection to "group" it; you only need to read each element once. Build a fresh counts and leave the input untouched.countBy([], fn) should return {}, and the loop body simply never runs, so the freshly created empty object is returned as-is. No special case needed — just don't assume there's at least one element.groupBy — same skeleton, but instead of counts[key] = (counts[key] ?? 0) + 1 you push the element into an array: (groups[key] ??= []).push(element). countBy is groupBy followed by reading each group's .length. If you have groupBy, you can derive countBy from it in one line.keyBy — when each key is expected to be unique, keyBy maps key → element (the last element wins on collisions) rather than key → count. Useful for turning a list of records into a lookup table: keyBy(users, 'id').partition — the special case where the iteratee is a boolean predicate and you want the two groups split out as [pass, fail] arrays rather than counted. partition(nums, (n) => n > 0) returns [positives, nonPositives]. It's groupBy with exactly two buckets, returned as a tuple instead of an object.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement countBy(collection, iteratee) — walk an array and tally how many elements fall into each bucket, where the bucket for an element is decided by iteratee. Think of sorting a deck of cards into piles by suit and then reporting "13 hearts, 13 spades, …", except the rule for which pile a card belongs to is something you pass in. This mirrors Lodash's _.countBy: the iteratee can be a function you call on each element, or a property-name string as shorthand for "group by this field."
// collection: T[] — the array to tally.
// iteratee: ((el: T) => K) — a function returning the bucket key, OR
// | string — a property name; shorthand for el => el[name].
// returns: Record<string, number>
// An object mapping each produced key (coerced to a string) to the
// COUNT of elements that produced it.
function countBy(collection, iteratee): Record<string, number>;
// Function iteratee: bucket floats by their integer part.
countBy([6.1, 4.2, 6.3], Math.floor);
// → { '6': 2, '4': 1 }
// Function iteratee: split into even and odd piles.
countBy([1, 2, 3, 4, 5], (n) => (n % 2 === 0 ? 'even' : 'odd'));
// → { odd: 3, even: 2 }
// Property-name iteratee: group people by their city field.
const people = [
{ name: 'Ada', city: 'London' },
{ name: 'Linus', city: 'Helsinki' },
{ name: 'Grace', city: 'London' },
];
countBy(people, 'city');
// → { London: 2, Helsinki: 1 }
countBy(items, 'city') behaves like countBy(items, (x) => x.city).6 and a key of '6' land in the same bucket; the returned object's keys are always strings.groupBy).countBy([], fn) returns {}.{} already responds to result['toString'] and result['constructor'] — make sure a bucket named "toString" counts like any other key rather than colliding with an inherited method.You'll walk an array once and tally how many elements fall into each bucket, where the bucket for an element is whatever key the iteratee produces.
Imagine a pile of receipts and you want to know how many came from each store. You go through them one at a time, and for each receipt you read the store name and add one to that store's tally. countBy is exactly that loop, with one twist: how you read the bucket off each element is a parameter. Sometimes the bucket is the result of a function — Math.floor(price), order.status === 'paid'. Sometimes it's just a field on the element, and the caller passes the field name as a string: countBy(receipts, 'store'). The output is an object whose keys are the buckets and whose values are the counts.
Hold three things in your head: the element you're looking at, the key the iteratee derives from it, and the counts object you're filling in. The whole algorithm is a single pass: for each element, compute its key, and bump that key's entry by one. Nothing is sorted, nothing is collected — you only ever store a running integer per key. The two iteratee forms (a function vs. a property-name string) are not two algorithms; they're two ways to get the key. Normalize them into a single "give me the key for this element" function up front, and the rest of the code never has to care which form the caller used.
The instinct is right — loop, derive a key, increment — but the first version usually leaves out the second iteratee form and reaches for a plain {}:
function countBy(collection, iteratee) {
const counts = {};
for (const element of collection) {
const key = iteratee(element); // assumes iteratee is always a function
if (counts[key]) {
counts[key] = counts[key] + 1;
} else {
counts[key] = 1;
}
}
return counts;
}
This handles the function case and the happy path, but it has two real bugs. First, it throws the moment someone calls countBy(people, 'city') — a string isn't callable, so iteratee(element) blows up with "iteratee is not a function." Second, the if (counts[key]) guard and the plain {} both stumble on keys that clash with inherited properties. With counts = {}, the expression counts['toString'] is the inherited toString function, which is truthy — so the code takes the + 1 branch and computes [Function] + 1, which is NaN. A bucket named "toString" silently corrupts.
function countBy(collection, iteratee) {
// Normalize the two iteratee forms into a single getter. A function is used
// directly; a string `name` becomes "read element[name]". From here down,
// the loop only ever calls getKey — it never re-checks the iteratee's type.
const getKey =
typeof iteratee === 'function'
? iteratee
: (element) => element[iteratee];
// Object.create(null) makes a bucket map with NO prototype, so keys like
// "toString" or "constructor" behave like any other key instead of colliding
// with inherited methods.
const counts = Object.create(null);
for (const element of collection) {
// String(...) makes the coercion explicit: a key of 6 and a key of "6"
// are the same bucket. (Object keys coerce to strings anyway; this just
// makes the intent obvious and keeps the ?? lookup honest.)
const key = String(getKey(element));
// `?? 0` treats a never-seen key as count 0, so the first sighting writes
// 1 and every repeat climbs from there.
counts[key] = (counts[key] ?? 0) + 1;
}
return counts;
}
module.exports = { countBy };
Three changes carry the fix. The getKey line collapses "function or string" into one getter, so the hot loop is type-agnostic. Object.create(null) removes the inherited-property landmine — there is no toString on the prototype to collide with. And (counts[key] ?? 0) + 1 replaces the brittle truthiness check: ?? only falls back when the left side is null or undefined, so a legitimately stored 0 would still be respected (counts never store 0, but the operator is the correct tool regardless).
Trace countBy([6.1, 4.2, 6.3], Math.floor) end to end.
iteratee is Math.floor, a function, so getKey is Math.floor. counts starts as an empty null-prototype object.
counts = {} (no prototype)
element 6.1 → key = String(Math.floor(6.1)) = '6'
→ counts['6'] = (undefined ?? 0) + 1 = 1
→ counts = { '6': 1 }
element 4.2 → key = String(Math.floor(4.2)) = '4'
→ counts['4'] = (undefined ?? 0) + 1 = 1
→ counts = { '6': 1, '4': 1 }
element 6.3 → key = String(Math.floor(6.3)) = '6'
→ counts['6'] = (1 ?? 0) + 1 = 2
→ counts = { '6': 2, '4': 1 }
return { '6': 2, '4': 1 }
The third element is the interesting one: '6' already exists with value 1, so 1 ?? 0 is 1 and the count climbs to 2. The keys come out in first-seen order — '6' then '4' — because that's the order JavaScript preserves for string keys on an object.
countBy(people, 'city') passes a string. If you write iteratee(element) unconditionally, V8 throws "iteratee is not a function" the instant a caller uses the property-name form. Normalize first: const getKey = typeof iteratee === 'function' ? iteratee : (el) => el[iteratee];.{} for the counts. const counts = {} inherits toString, constructor, hasOwnProperty, and more. Counting a value of "toString" reads the inherited function: counts['toString'] is truthy and [Function] + 1 is NaN, so that bucket is silently wrong. Use Object.create(null) (or guard every lookup with Object.prototype.hasOwnProperty.call(counts, key)), so inherited names can't shadow real buckets.if (counts[key]) instead of ?? 0. Truthiness is the wrong test. It happens to work for positive counts, but it's exactly what makes the inherited-toString bug fire (the inherited function is truthy). Reach for (counts[key] ?? 0) + 1 so the "first time" case is decided by presence, not by whether the current value is truthy.[1, 2, 1] by identity and bucketing ['1', '2', '1'] by identity produce the same object — { '1': 2, '2': 1 } — because object keys are always strings. If a caller expects numeric keys back, that's not possible with a plain object; they'd need a Map. Make the coercion explicit with String(...) so this isn't a surprise.element[iteratee] or calling iteratee(element) is fine — those don't change anything. But don't be tempted to sort or splice collection to "group" it; you only need to read each element once. Build a fresh counts and leave the input untouched.countBy([], fn) should return {}, and the loop body simply never runs, so the freshly created empty object is returned as-is. No special case needed — just don't assume there's at least one element.groupBy — same skeleton, but instead of counts[key] = (counts[key] ?? 0) + 1 you push the element into an array: (groups[key] ??= []).push(element). countBy is groupBy followed by reading each group's .length. If you have groupBy, you can derive countBy from it in one line.keyBy — when each key is expected to be unique, keyBy maps key → element (the last element wins on collisions) rather than key → count. Useful for turning a list of records into a lookup table: keyBy(users, 'id').partition — the special case where the iteratee is a boolean predicate and you want the two groups split out as [pass, fail] arrays rather than counted. partition(nums, (n) => n > 0) returns [positives, nonPositives]. It's groupBy with exactly two buckets, returned as a tuple instead of an object.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.