Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Make Any Object Spreadable: Symbol.iterator in Practice

C
Carlo Fontanos
· 2 min read

Ever wondered why you can for...of a Map but not your own collection class? It's not magic blessed by the engine - it's one well-known method name, and your objects can have it too.

class PlaylistQueue {
    #tracks = [];
    add(track) { this.#tracks.push(track); return this; }

    *[Symbol.iterator]() {
        yield* this.#tracks.filter(t => !t.skipped);
    }
}

const queue = new PlaylistQueue().add(a).add(b).add(c);

for (const track of queue) play(track);      // just works
const remaining = [...queue];                 // spread works
const [next, ...rest] = queue;                // destructuring works
Array.from(queue, t => t.title);              // from() works
new Set(queue);                               // even Set construction

One generator method - the computed name [Symbol.iterator] with a * - and every language feature that consumes iterables accepts your class. That's the whole protocol: any object whose Symbol.iterator returns an iterator is an iterable, and generators produce iterators for free.

Why bother, when you could expose the array?

Because the iterator controls what iteration means without exposing internals. The playlist above skips skipped tracks during iteration - callers can't tell and don't care. A date-range object can yield days lazily. A paginated collection can yield only loaded items. You're defining the class's public traversal semantics in one place:

class DateRange {
    constructor(start, end) { this.start = start; this.end = end; }

    *[Symbol.iterator]() {
        for (let d = new Date(this.start); d <= this.end; d.setDate(d.getDate() + 1)) {
            yield new Date(d);
        }
    }
}

for (const day of new DateRange(monday, friday)) renderColumn(day);

No array of 365 Dates materialized unless someone spreads it - laziness inherited straight from the generator. (PHP mirrors this whole idea with the Traversable/IteratorAggregate interfaces; the concept transfers 1:1.)

Small print that saves confusion

  • for...of consumes iterables; for...in enumerates keys. Adding Symbol.iterator doesn't change for...in, and you almost never want for...in on collections anyway.
  • Each for...of call invokes the method again - a generator gives you a fresh iterator per loop, so iterating twice works. If you return a single shared iterator instead, the second loop finds it exhausted. Generators get this right by default.
  • Objects are the one built-in that isn't iterable - spread-in-array [...obj] throws. That's deliberate ({...obj} object spread uses different machinery). Object.entries() is the bridge.

The async twin - Symbol.asyncIterator - is exactly this pattern with await sprinkled in, and it's what powers for await over paginated APIs. Learn one protocol, get both.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading