Iteration is a Contract: Making Plain Objects Iterable with Symbol.iterator
An iterable is a contract for producing values in order. Adding Symbol.iterator to a plain object makes for...of and spread consume it consistently.
Iteration is a Contract: Making Plain Objects Iterable with Symbol.iterator
One-sentence takeaway: An iterable is a contract for producing values in order. Implementing Symbol.iterator on a plain object makes for...of and the spread operator consume it consistently.
Background / Problem
A plain object (Object) is great for key–value storage, but it isn't iterable by default. That often leads to patterns like:
- Converting with
Object.keys()/Object.values()whenever you need ordering. - Duplicating iteration rules across call sites, which increases maintenance mistakes.
The key point is simple: move the iteration rule into the object so the call site can stay uniform.
Core Concepts
Iterable and Iterator
- Iterable: an object that provides
Symbol.iterator, which returns an iterator. - Iterator: an object with
next(). Eachnext()returns{ value, done }.
For the exact contract, refer to official docs:
→ Expected outcome / What changes: the call site only uses iteration syntax (for...of, spread), while the object owns ordering and termination.
Approach
You can reach the goal ("make a plain object iterable") in a few ways:
1) Implement Symbol.iterator directly
- Why: to fully control ordering/filtering/termination
- Expected: call sites unify into
for...of/ spread
2) Convert with Object.values() and iterate
- Why: fastest way to get something working
- Expected: simple, but conversion logic stays scattered
3) Use a generator
- Why: focus on "what to yield" without manual
donehandling - Expected: better readability as logic grows
Below, we start with the direct protocol implementation, then show the generator alternative.
Implementation (Code)
1) Minimal: add Symbol.iterator to a plain object
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };
obj[Symbol.iterator] = function () {
let i = 1;
return {
next() {
return i > 4 ? { done: true } : { value: i++, done: false };
},
};
};
console.log("--- for...of ---");
for (const value of obj) console.log("value:", value);
console.log("--- spread ---");
console.log(...obj);→ Expected outcome / What changes: for...of obtains an iterator via Symbol.iterator() and repeatedly calls next() until done: true. Spread consumes values the same way.
2) Practical: iterate over the object's own values
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };
obj[Symbol.iterator] = function () {
const values = Object.values(this);
let idx = 0;
return {
next() {
if (idx >= values.length) return { done: true };
return { value: values[idx++], done: false };
},
};
};
console.log([...obj]); // [1, 2, 3, 4]→ Expected outcome / What changes: iteration is anchored to the object's data, so call sites can stay consistent.
3) Alternative: a generator-based iterator
const obj = { 0: 1, 1: 2, 2: 3, 3: 4 };
obj[Symbol.iterator] = function* () {
for (const v of Object.values(this)) yield v;
};
console.log([...obj]); // [1, 2, 3, 4]→ Expected outcome / What changes: no manual next()/done bookkeeping—only the yield order remains.
Verification Checklist
for...of works....obj) outputs the same order.done: true.Common Pitfalls / FAQ
Q1. How is this different from for...in?
for...in enumerates property keys; for...of consumes iterable values.
Q2. Is adding Symbol.iterator to a plain object always a good idea?
Depending on team conventions or library assumptions, it can break the expectation that "plain objects are not iterable." In such cases, use a wrapper object or restrict the behavior to specific types.
Q3. Is Object.values() order always safe to rely on?
It depends on property ordering rules. If order is fundamental to the model, prefer structures where order is part of the design (like arrays or Map).
Summary
- An iterable is a contract for producing values.
Symbol.iteratoris the entry point;next()drives consumption.- Encapsulating iteration rules inside the object simplifies call sites.
- Generators can keep complex iteration logic readable.
Conclusion
When iteration logic is scattered across call sites, mistakes grow with it. Implementing Symbol.iterator lets the object own "how to iterate," leaving only iteration syntax at the call site.