I have a Ruby background and if I wanted to extend all enumerable objects I would simply add a method to the Enumerable
module. Which is included by Array
, Hash
(dictionary), Set
, etc.
I was wondering if there is an easy way to do the same in JavaScript and attach methods to all objects that implement the iterable protocol. The only thing I could come up with is to write a wrapper for iterables and then define methods there. (Snippet below made for demonstration purposes.)
My current solution feels fairly clunky. Is there a better way of adding methods to iterable objects that I'm missing?
class Iterable {
static *forEach(iterable, fn) {
for (const value of iterable) {
fn(value);
yield value;
}
}
static *map(iterable, fn) {
for (const value of iterable) {
yield fn(value);
}
}
static *filter(iterable, fn) {
for (const value of iterable) {
if (fn(value)) yield value;
}
}
static *take(iterable, amount) {
const iterator = iterable[Symbol.iterator]();
for (;amount > 0; --amount) {
const {done, value} = iterator.next();
if (done) break;
yield value;
}
}
// ... additional iterable methods ...
constructor(iterable) {
this.iterable = iterable;
}
[Symbol.iterator]() {
return this.iterable[Symbol.iterator]();
}
toArray() {
return Array.from(this);
}
forEach(fn) {
return new this.constructor(Iterable.forEach(this, fn));
}
map(fn) {
return new this.constructor(Iterable.map(this, fn));
}
filter(fn) {
return new this.constructor(Iterable.filter(this, fn));
}
take(amount) {
return new this.constructor(Iterable.take(this, amount));
}
// ... additional iterable methods ...
}
const result = new Iterable([0,1,2,3,4,5,6,7,8,9])
.forEach(nr => console.log("initial:", nr))
.filter(nr => nr % 2)
.forEach(nr => console.log("filtered:", nr))
.map(nr => nr * 100)
.forEach(nr => console.log("mapped:", nr))
.take(2)
.forEach(nr => console.log("taken:", nr));
console.log("The iterable hasn't started iterating yet.");
console.log("result:", result.toArray()); // <- toArray will start the iteration process
The iterable protocol does not require the object to have a certain prototype, so there is no prototype to put your methods on.
However, ECMAScript 2025 introduced Iterator.prototype
from which all core-JS iterators inherit. So if you would get such an instance for your iterable, then that opens the door to some iterator helper methods. You could add your own to Iterator.prototype
if you really wanted to.
In case you start with an array, then you can get such Iterator
instance with its .values()
method, and then you can chain .map
, .filter
, ... which are now native to JavaScript:
const arr = [1,2,3,4,5,6,7,8,9];
const it = arr.values().map(x => x ** 2).filter(x => x > 10);
console.log(it.next().value);
In case your iterable is some custom iterable that has no methods to get an iterator, then just use Iterator.from(iterable)
. It also works with arrays, but also any iterable:
const arr = [1,2,3,4,5,6,7,8,9];
const it = Iterator.from(arr).map(x => x ** 2).filter(x => x > 10);
console.log(it.next().value);