Reduce() + Map for counting related entries
The problem (Astro Content Collections)
In my data layer, I have:
- a
producerscollection - a
winescollection - each wine stores a reference to its producer (e.g.
wine.data.producer)
I want to show how many wines each producer has in a ProducerCard.
Important detail: the reference is one-way (Wine → Producer). There is no automatic “reverse list” (Producer → Wines) unless I model it myself.
So the simplest approach is:
- load all wines once
- count how many belong to each producer
- attach the count to each producer card object
What reduce() does
reduce() “reduces” an array into one final value by repeatedly calling a callback.
Per the ECMAScript spec:
- the callback is called with four arguments:
previousValue,currentValue,currentIndex, and the array object - if you pass an
initialValue, then the firstpreviousValueis thatinitialValue - otherwise,
previousValuestarts as the first array element (andcurrentValuestarts as the second)
Source: ECMAScript Array.prototype.reduce
In practice, I usually use only the first two parameters:
- accumulator (the running result)
- current item (the array element being processed right now)
What a Map is (and why it fits counting)
A Map is a collection of key/value pairs where:
- keys can be any JS value (not just strings)
- each key is unique (one value per key)
Source: ECMAScript Map Objects definition. (https://tc39.es/ecma262/multipage/keyed-collections.html#sec-map-objects)
Two methods matter here:
map.get(key)returns the value for that key, orundefinedif not presentmap.set(key, value)adds the key/value pair, or updates the value if the key already exists (Defined in the same ECMAScript Map section above.)
For counting:
- key =
producerId(string) - value = count (number)
Why new Map<string, number>() (TypeScript generics)
In TypeScript, Map<K, V> is a generic type:
Kis the key typeVis the value type
So Map<string, number> means:
- keys must be strings
- values must be numbers
TypeScript generics (official docs):
https://www.typescriptlang.org/docs/handbook/2/generics.html
TypeScript also explicitly uses examples like Map<K, V> to explain generic data structures:
https://www.typescriptlang.org/docs/handbook/2/objects.html
Why ?? 0 is used
When you do map.get(producerId):
- if this producer hasn’t been seen yet, it returns
undefined
So you convert “missing” into 0 before adding 1.
?? chooses the right-hand side only if the left-hand side is null or undefined.
Nullish coalescing spec (TC39):
https://tc39.es/proposal-nullish-coalescing/
The counting code (clear version)
This builds a Map that ends up like:
"alpha" => 2"bravo" => 1
const wines = await getCollection("wines")
const wineCountByProducerId = wines.reduce(
(countsByProducerId, wine) => {
const producerId = wine.data.producer.id
const previousCount = countsByProducerId.get(producerId) ?? 0
const nextCount = previousCount + 1
countsByProducerId.set(producerId, nextCount)
return countsByProducerId
},
new Map<string, number>()
)
What happens step-by-step (3 wines, 2 producers)
Given wines:
- Wine A → producer
"alpha" - Wine B → producer
"bravo" - Wine C → producer
"alpha"
Initial accumulator:
Map {}
Iteration 1 ("alpha"):
get("alpha")→undefinedpreviousCount = 0,nextCount = 1set("alpha", 1)- Map is now:
{"alpha" => 1}
Iteration 2 ("bravo"):
get("bravo")→undefinedpreviousCount = 0,nextCount = 1set("bravo", 1)- Map is now:
{"alpha" => 1, "bravo" => 1}
Iteration 3 ("alpha"):
get("alpha")→1previousCount = 1,nextCount = 2set("alpha", 2)- Map is now:
{"alpha" => 2, "bravo" => 1}
Final result of reduce() is that Map.
The “one-liner” version (same logic, less readable)
countsByProducerId.set(
producerId,
(countsByProducerId.get(producerId) ?? 0) + 1
)
Same steps:
- read count with
get - default to 0 if missing
- add 1
- write back with
set
Quick rule of thumb
-
If the reference is only on the “many” side (Wine → Producer), and you want counts for the “one” side (Producer), you must:
- query the many side (wines)
- aggregate/group by the referenced id