Reduce() + Map for counting related entries

The problem (Astro Content Collections)

In my data layer, I have:

  • a producers collection
  • a wines collection
  • 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:

  1. load all wines once
  2. count how many belong to each producer
  3. 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 first previousValue is that initialValue
  • otherwise, previousValue starts as the first array element (and currentValue starts 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, or undefined if not present
  • map.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:

  • K is the key type
  • V is 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")undefined
  • previousCount = 0, nextCount = 1
  • set("alpha", 1)
  • Map is now: {"alpha" => 1}

Iteration 2 ("bravo"):

  • get("bravo")undefined
  • previousCount = 0, nextCount = 1
  • set("bravo", 1)
  • Map is now: {"alpha" => 1, "bravo" => 1}

Iteration 3 ("alpha"):

  • get("alpha")1
  • previousCount = 1, nextCount = 2
  • set("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:

  1. read count with get
  2. default to 0 if missing
  3. add 1
  4. 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