JavaScript & the runtime

Collections, iterators and generators

Map, Set and their 2025 methods, WeakMap and WeakRef, the iteration protocol, generators and async iteration, iterator helpers, grouping and upsert, change-by-copy arrays, typed arrays and base64, and web streams.

Most of what JavaScript gained between 2023 and 2026 is here: Set algebra, iterator helpers, grouping, upsert, Array.fromAsync, base64 on Uint8Array. Interviewers use collections questions to see whether you pick the right structure ("why a Map here?") and whether you know the protocol underneath for...of, which is what makes generators, streams and lazy pipelines click.

Know

  • Map versus object: any key type, insertion order guaranteed, size, no prototype to collide with (__proto__), faster for frequent add/remove and unknown key sets; objects stay for fixed-shape records and JSON. Set for uniqueness and membership (has is O(1) versus includes O(n)); both use SameValueZero (NaN equal to itself, +0/-0 equal).
  • Set methods (ES2025): union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom, returning new Sets and accepting set-likes.
  • WeakMap / WeakSet hold object keys weakly: no enumeration, no size, entries vanish when the key is collected. The right tool for caches keyed by objects (DOM nodes, component instances) and private data. WeakRef and FinalizationRegistry exist for advanced cases and are not a cleanup mechanism you can rely on.
  • The iteration protocol: an iterable has [Symbol.iterator]() returning an iterator with next() → { value, done } (optional return()/throw()). Arrays, strings, Map, Set, arguments, NodeList, typed arrays are iterable; for...of, spread, destructuring, Array.from, Promise.all consume iterables. Async iterables use [Symbol.asyncIterator] and for await...of.
  • Generators (function*) build iterators from imperative code: yield pauses, yield* delegates, return finishes, return() from the consumer triggers finally for cleanup; they are lazy and can be infinite. Async generators (async function*) yield promises and are how you wrap paginated APIs or streams.
  • Iterator helpers (ES2025) on every iterator: map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, find, plus Iterator.from(); lazy, so take(5) on an infinite generator terminates. Iterator.concat (ES2026) chains iterables; Iterator.zip ("joint iteration") is finished for ES2027.
  • Grouping and upsert: Object.groupBy / Map.groupBy (ES2024) replace reduce-based grouping; Map.prototype.getOrInsert / getOrInsertComputed (ES2026 "upsert") replace the if (!map.has(k)) map.set(k, init) dance.
  • Arrays: change-by-copy toSorted, toReversed, toSpliced, with (ES2023) versus the mutating sort/reverse/splice; at(-1), findLast, flat/flatMap; Array.fromAsync (ES2026) collects an async iterable. sort is stable and compares as strings without a comparator.
  • Binary: ArrayBuffer (resizable and transfer() since ES2024), typed arrays, DataView, Float16Array (ES2025); Uint8Array.fromBase64/toBase64/fromHex/toHex (ES2026) end the btoa workarounds.
  • Web streams: ReadableStream (async iterable), WritableStream, TransformStream, pipeThrough/pipeTo, backpressure built in; fetch bodies are streams (response.body), TextDecoderStream/TextEncoderStream convert, and Nitro can return a ReadableStream as a response. Streams are how you process a 200 MB response without holding it in memory.
  • structuredClone deep-copies Map, Set, Date, ArrayBuffer and more (not functions or DOM nodes); spread is shallow.

How it works

Right structure, right method:

app/utils/collections.ts
// Index by id once, look up O(1) instead of nested find (O(n²)).
const byId = new Map(users.map(u => [u.id, u] as const))
const withOrders = orders.map(o => ({ ...o, user: byId.get(o.userId) }))

// Grouping (ES2024) and upsert (ES2026)
const byRole = Map.groupBy(users, u => u.role)          // Map<Role, User[]>
const counts = new Map<string, number>()
for (const u of users) counts.set(u.country, counts.getOrInsert(u.country, 0) + 1)

// Set algebra (ES2025)
const admins = new Set(users.filter(u => u.role === 'admin').map(u => u.id))
const online = new Set(presence.ids)
const onlineAdmins = admins.intersection(online)
const offlineAdmins = admins.difference(online)

// Lazy pipeline with iterator helpers: nothing runs until take() pulls
function* naturals() { let n = 1; while (true) yield n++ }
const firstFiveEvenSquares = naturals().filter(n => n % 2 === 0).map(n => n * n).take(5).toArray()

// Non-mutating sort for a computed, then base64 for a binary payload (ES2026)
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
const b64 = new TextEncoder().encode(JSON.stringify(sorted)).toBase64()

An async generator over a paginated API, consumed lazily, and Array.fromAsync to collect it:

app/utils/paginate.ts
export async function* paginate<T>(url: string, signal?: AbortSignal) {
  let cursor: string | null = null
  do {
    const page: { items: T[], next: string | null } = await $fetch(url, { query: { after: cursor }, signal })
    yield* page.items // hand items out one by one; the caller decides when to stop
    cursor = page.next
  } while (cursor)
}

// Stop after the first match without fetching the rest of the pages:
for await (const item of paginate<Product>('/api/products')) {
  if (item.sku === wanted) { found = item; break } // break calls the generator's return(): the loop ends, no more fetches
}

// Or collect everything (careful with size):
const all = await Array.fromAsync(paginate<Product>('/api/products'))

Streaming a large response through a transform without buffering it:

server/api/export.get.ts
export default defineEventHandler(async (event) => {
  const upstream = await fetch('https://warehouse.example.com/rows.ndjson')
  const toCsv = new TransformStream<string, string>({
    transform(chunk, controller) {
      for (const line of chunk.split('\n').filter(Boolean)) {
        const row = JSON.parse(line) as { id: string, name: string }
        controller.enqueue(`${row.id},"${row.name.replaceAll('"', '""')}"\n`)
      }
    },
  })
  setResponseHeader(event, 'content-type', 'text/csv; charset=utf-8')
  return upstream.body!
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(toCsv)
    .pipeThrough(new TextEncoderStream())
})
Gotcha· Sorting numbers without a comparator

[10, 9, 1].sort() gives [1, 10, 9]: the default comparison is by string. Always pass a comparator, and for user-visible strings use localeCompare or an Intl.Collator so accents and case sort correctly.

Gotcha· Mutating in a computed

computed(() => items.value.sort(...)) mutates the source array and triggers itself. toSorted (ES2023) is the non-mutating version and is exactly what a derived value should use.

Verify before the interview:

Browser and Node availability of the ES2026 additions (Map.prototype.getOrInsert, Iterator.concat, Uint8Array.toBase64) as of the interview week; polyfill or transpile until they are Baseline.

docs ↗

Exercise

Exercise
  • Rewrite a nested find join over two arrays with a Map index and measure both at 10,000 items.
  • Implement the paginated async generator against a fake API and prove that break stops the fetching by logging each page request.
  • Stream a 100 MB NDJSON file through the CSV transform and watch memory in the Node process stay flat.

Be able to say

Be able to say· When do you use a Map or a Set instead of an object or an array?

"A Map when keys are dynamic or not strings, when insertion order or size matters, or when I add and remove often: lookups are O(1) with any key type and there is no prototype to collide with. Objects stay for fixed-shape records and JSON. A Set when the question is membership or uniqueness, because has is constant time where includes scans, and since ES2025 Sets have union, intersection and difference built in. WeakMap when the key is an object whose lifetime I do not control, so the entry disappears with it. And I reach for the iteration protocol underneath all of them, generators and iterator helpers, when I want lazy pipelines that stop early."

Be able to say· What is the iteration protocol, and why do generators matter?

"An iterable is anything with a Symbol.iterator method that returns an iterator: an object whose next() returns { value, done }. for...of, spread, destructuring, Array.from and Promise.all all consume that protocol, and Map, Set, strings and arrays implement it. Generators are the easiest way to produce one: yield pauses the function and hands out a value on demand, so sequences can be lazy or infinite, and return() from the consumer runs the generator's cleanup. Async generators do the same over promises, which is how I wrap a paginated API so for await...of can stop after the first match without fetching the remaining pages. Iterator helpers since ES2025 add map, filter and take directly on iterators, so the pipeline stays lazy."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.