js-xiterable
v0.3.0
Published
Make ES6 Iterators Functional Again
Maintainers
Readme
js-xiterable
Make ES6 Iterators Functional Again
Synopsis
Suppose we have a generator like this.
function* count(n) {
for (let i = 0; i < n; i++) yield i;
};We make it more functional like this.
import {Xiterable} from './xiterable.js';
const xcount = n => new Xiterable(() => count(n));
const tens = xcount(10);
const odds = tens.filter(v=>v%2).map(v=>v*v);
const zips = tens.zip(odds);
[...tens]; // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...odds]; // [ 1, 9, 25, 49, 81]
[...zips]; // [[0, 1], [1, 9], [2, 25], [3, 49], [4, 81]]In other words, this module make any iterables work like Array, with .map, .filter and so on.
Install
npm install js-xiterableUsage
locally
import {
Xiterable,
xiterable, zip, zipWith, xrange, repeat
} from './xiterable.js';You don't even have to install if you import from CDNs.
import {Xiterable} from 'https://cdn.jsdelivr.net/npm/[email protected]/xiterable.min.js';Or plain old <script> tags — the UMD version umd/xiterable.js makes Xiterable globally available.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/xiterable.min.js"></script>
<script>
// now you can access Xiterable
const squares = [...Xiterable.xrange(4).map(v => v * v)];
</script>node.js REPL
% node
Welcome to Node.js v24.19.0.
Type ".help" for more information.
> const $X = await import('js-xiterable')
undefined
> $X
[Module: null prototype] {
Xiterable: [class Xiterable],
isAnyInt: [Function: isAnyInt],
isIterable: [Function: isIterable],
repeat: [Function: bound repeat],
version: '0.3.0',
xiterable: [Function: bound of],
xrange: [Function: bound xrange],
zip: [Function: bound zip],
zipWith: [Function: bound zipWith]
}
> [...$X.xrange(4).map(v => v * v)]
[ 0, 1, 4, 9 ]commonjs (node.js)
require() works too, via commonjs/xiterable.js.
const { Xiterable, xrange } = require('js-xiterable');Description
This module makes any given iterables behave like an array with all functional methods like .map(), .filter(), .reduce() and so on. But for methods that Array.prototype returns an instance of Array, an instace of Xiterable is returned. An Xiterable instance are:
like an instance of
Arraythat has.map(),.filter(),.reduce()…unlike an instance of
Arraythat demands the storage for elements. Elements are generated on demand.
[...Array(1e9).keys()].slice(0,10) // gets stuck with a billion elements
[...xrange(1e9).slice(0,10)] // same expected result instantly.All elements are lazily generated. They are not generated until needed.
That is why .filter marks the result infinite, even though it is finite. You cannot estimate the number of elements until you apply the predicate function.
constructor
from any built-in iterables...
new Xiterable([0,1,2,3]);
new Xiterable('0123');
new Xiterable(new Uint8Array([0,1,2,3]))or your custom generator (with no argument)...
let it = new Xiterable(function *() {
for (let i = 0; true; i++) yield i;
});
[...it.take(8)]; // [ 0, 1, 2, 3, 4, 5, 6, 7]
[...it.take(8).reversed()] // throws TypeError;Generators are treated as an infinite iterable. But you can override it by giving its length for the 2nd argument and the implentation of nth for the 3rd argument. see .at() and .map() for more example.
let it = new Xiterable(function *() {
for (let i = 0; true; i++) yield i;
}, Number.POSIVE_INFINITY, n => n);
it.at(42); // 42
it.take(42).reversed().at(0) // 41A factory function is also exported as xiterable.
import {xiterable as $X} from 'js-xiterable';
$X('01234567').zip('abcdefgh').map(v=>v.join('')).toArray(); /* [
'0a', '1b', '2c', '3d', '4e', '5f', '6g', '7h'
] */Instance Methods and Properties
.toArray()
Returns [...this] unless this is infinite, in which case throws RangeError. It takes longer to spell than [...this] but slightly safer. Note that methods like .filter() mark their result infinite because the length cannot be known in advance, so .toArray() throws on them too even when the source is finite. Use [...this] when you know better.
.at()
.at(n) returns the nth element of this if the original itertor has .at or .nth or Array-like (can access nth element via [n]. In which case at() is auto-generated).
The function was previously named nth(), which is still an alias of at() for compatibility reason.
Unlike [n], .at(n) accepts BigInt and negatives.
let it = xiterable('javascript');
it.at(0); // 'j'
it.at(0n); // 'j'
it.at(9); // 't'
it.at(-1); // 't'
it.at(-1n); // 't'
it.at(-10); // 'j'It raises exceptions on infinite (and indefinite) iterables
it = xiterable(function*(){ for(;;) yield 42 }); // infinite
[...it.take(42)]; // Array(42).fill(42)
it.at(0); // throws TypeError;it = xiterable('javascript');
it = it.filter(v=>!new Set('aeiou').has(v)); // indefinite
[...it]; // ['j', 'v', 's', 'c', 'r', 'p', 't']
it.at(0); // throws TypeError;BigInt is sometimes necessary when you deal with large -- combinatorially large -- iterables like js-combinatorics handles.
import * as $C from 'js-combinatorics';
let it = xiterable(new $C.Permutation('abcdefghijklmnopqrstuvwxyz'));
it = it.map(v=>v.join(''));
it.at(0); // 'abcdefghijklmnopqrstuvwxyz'
it.at(-1); // 'zyxwvutsrqponmlkjihgfedcba'
it.at(403291461126605635583999999n) === it.at(-1); // true.length
The (maximum) number of elements in the iterable. For infinite (or indefinite iterables like the result of .filter()) Number.POSITIVE_INFINITY is set.
xrange().length; // Number.POSITIVE_INFINITY
xrange().take(42).length // 42
xrange().take(42).filter(v=>v%2).length; // Number.POSITIVE_INFINITYThe number can be BigInt for very large iterable.
it = xiterable(new $C.Permutation('abcdefghijklmnopqrstuvwxyz'));
it.length; // 403291461126605635584000000nYou can tell if the iterable is infinite or indefinite via .isEndless.
xrange().isEndless; // true
xrange().take(42).isEndless // false
xrange().take(42).filter(v=>v%2).isEndless; // true.map()
.map(fn, thisArg?) works just like Array.prototype.map except:
.mapof this module works with infinite iterables.if
thisis finite with working.nth, the resulting iterable is also reversible with.reversedand random-accissible via.nth.
it = xiterable(function*(){ let i = 0; for(;;) yield i++ });
[...it.map(v=>v*v).take(8)] // [0, 1, 4, 9, 16, 25, 36, 49]
it.at(42); // throws TypeError
it = xiterable(it.seed, it.length, n=>n); // installs nth
it.at(42); // 41.filter()
.filter(fn, thisArg?) works just like Array.prototype.filter except:
.filterof this module works with infinite iterables.unlike .map() the resulting iterable is always marked infinite because there is no way to know its length lazily, that is, prior to iteration. See .length for more examples.
it = xiterable('javascript');
it.length; // 10
it = it.filter(v=>!new Set('aeiou').has(v));
it.length; // Number.POSITIVE_INFINITY
[...it].length; // 7
[...it] // [ 'j', 'v', 's', 'c', 'r', 'p', 't' ].mapFilter()
.mapFilter(fn, thisArg?) works just like .filter() but instead of dropping elements, it is replaced with undefined. That way the number of elements remains unchanged so you can use .at() and .reversed().
it = xiterable('javascript');
it.length; // 10
it = it.mapFilter(v=>!new Set('aeiou').has(v));
it.length; // 10
[...it].length; // 10
[...it]; /* [
'j', undefined, 'v', undefined,
's', 'c', 'r', undefined, 'p', 't'
] */.take()
.take(n) returns an iterable with the first n elements from this.
If n <= this.length it is a no-op.
[...xrange().take(8)]; // [0, 1, 2, 3, 4, 5, 6, 7]
[...xrange().take(4).take(8)]; // [0, 1, 2, 3].drop()
.drop(n)returns an iterable without the first n elements from this.
If n <= this.length an empty iterable is returned.
[...xrange(8).drop(4)]; // [4, 5, 6, 7]
[...xrange(4).drop(8)]; // []Note the infinite iterable remains infinite even after you .drop(n)
xrange().drop(8).length; // Number.POSITIVE_INFINITY
[...xrange().drop(8).take(4)]; // [ 8, 9, 10, 11 ].takeWhile()
.takeWhile(fn, thisArg?) returns an iterable with which iterates till fn is no longer true. Similar to .filter but unlinke .filter() the iterator terminates at the first element where fn() returns false.
let it = xiterable('javascript');
let fn = v=>!new Set('aeiou').has(v);
[...it.filter(fn)]; // ['j', 'v', 's', 'c', 'r', 'p', 't']
[...it.takeWhile(fn)]; // ['j'].filled()
.filled(value) returns an iterator with all elements replaced with value. See also Xiterable.repeat.
.reversed()
.reversed() returns an iterator that returns elements in reverse order. this must be finite and random-accesible via .at() or exception is thrown.
[...xrange().take(4).reversed()]; // [3, 2, 1, 0]
[...xrange().reversed()]; // throws RangeError.zip()
.zip(...args) zips iterators in the args. Static version also available.
[...Xiterable.xrange().zip('abcd')] // [[0,"a"],[1,"b"],[2,"c"],[3,"d"]]Instance methods found in Array.prototype
The following methods in Array.prototype are supported as follows. For any method meth, [...iter.meth(arg)] deeply equals to [...iter].meth(arg).
| method | available? | Comment |
|:--------------|:----:|:---------|
|concat | ✔︎ | |
|copyWithin | ❌ | mutating |
|entries | ✔︎ | |
|every | ✔︎ | |
|fill | ❌ | mutating ; see .filled |
|filter | ✔︎ | see filter |
|find | ✔︎ | |
|findIndex | ✔︎ | |
|flat | ✔︎ | |
|flatMap | ✔︎ | |
|forEach | ✔︎ | |
|includes | ✔︎*| * throws RangeError on infinite iterables if the 2nd arg is negative |
|indexOf | ✔︎*| * throws RangeError on infinite iterables if the 2nd arg is negative |
|join | ✔︎ | |
|keys | ✔︎ | |
|lastIndexOf | ✔︎*| * throws RangeError on infinite iterables if the 2nd arg is negative |
|map | ✔︎ | see map |
|pop | ❌ | mutating |
|push | ❌ | mutating |
|reduce | ✔︎* | * throws RangeError on infinite iterables |
|reduceRight | ✔︎* | * throws RangeError on infinite iterables |
|reverse | ❌ | mutating. See reversed |
|shift | ❌ | mutating |
|slice | ✔︎* | * throws RangeError on infinite iterables if any of the args is negative |
|some | ✔︎ | |
|sort | ❌ | mutating |
|splice | ❌ | mutating |
|unshift | ❌ | mutating |
|filter | ✔︎ | |
Mutating functions (functions that change
this) are deliberately made unavailable. e.g.pop,push…Functions that need toiterate backwards do not work on infinite iterables. e.g.
lastIndexOf(),reduceRight()…
Static methods
They are also exported so you can:
import {repeat,xrange,zip,zipWith} from 'js-xiterable'Examples below assumes
import {Xiterable} from 'js-xiterable'.Examples below assumes
Xiterable.zip
Zips iterators in the argument.
[...Xiterable.zip('0123', 'abcd')] // [[0,"a"],[1,"b"],[2,"c"],[3,"d"]]Xiterable.zipWith
Zips iterators and then feed it to the function.
[...Xiterable.zipWith((a,b)=>a+b, 'bcdfg', 'aeiou')] // ["ba","ce","di","fo","gu"]Xiterable.xrange
xrange() as Python 2 (or range() of Python 3).
for (const i of Xiterable.xrange()){ // infinite stream of 0, 1, ...
console.log(i)
}[...Xiterable.xrange(4)] // [0, 1, 2, 3]
[...Xiterable.xrange(1,5)] // [1, 2, 3, 4]
[...Xiterable.xrange(1,5,2)] // [1, 3] Xiterable.repeat
Returns an iterator with all elements are the same.
for (const i of Xiterable.repeat('spam')) { // infinite stream of 'spam'
console.log(i)
}[...Xiterable.repeat('spam', 4)] // ['spam', 'spam', 'spam', 'spam']Comparison with built-in Iterator helpers
When this module was first written, iterators in JavaScript had nothing but .next(). You could for…of them and spread them into arrays, but .map(), .filter(), .take() and friends were nowhere to be found. That was the whole reason this module exists.
Since then, Iterator helpers have landed in the language (ES2025) and are available in recent runtimes: Node.js 22+, Chrome 122+, Firefox 131+, Safari 18.4+ and Bun 1.1+. Iterator.prototype now has .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray(), .forEach(), .some(), .every() and .find(), and Iterator.from() wraps any iterable or iterator-like object.
// no module needed on a modern runtime
function* naturals() { for (let i = 0; true; i++) yield i; }
naturals().map(v => v * v).filter(v => v % 2).take(3).toArray(); // [ 1, 9, 25 ]So do you still need this module? If all you need is sequential, one-pass transforms on a modern runtime, no. This module still offers a few things the built-ins do not.
Re-iterable vs. one-shot
A built-in iterator is consumed as you go. Once exhausted it stays exhausted, and so does anything derived from it.
const it = [1, 2, 3].values().map(v => v * v);
[...it]; // [ 1, 4, 9 ]
[...it]; // []An Xiterable wraps an iterable, not an iterator, so every iteration starts afresh.
const xi = xiterable([1, 2, 3]).map(v => v * v);
[...xi]; // [ 1, 4, 9 ]
[...xi]; // [ 1, 4, 9 ]Random access and length
Because it knows how long it is and how to compute the n-th element, an Xiterable supports .length, .at(), negative indexes in .slice() and .reversed(), all without materializing the elements. Built-in iterators are sequential only.
xrange(1e9).map(v => v * 2).at(-1); // 1999999998, instantly
[...xrange(1e9).slice(-3)]; // [ 999999997, 999999998, 999999999 ]
[...xrange(1e9).reversed().take(3)]; // [ 999999999, 999999998, 999999997 ]Method comparison
| method | Iterator.prototype | Xiterable.prototype |
|:-------|:----:|:----:|
| .map(), .filter(), .flatMap() | ✔︎ | ✔︎ |
| .take(), .drop() | ✔︎ | ✔︎ |
| .reduce(), .forEach(), .toArray() | ✔︎ | ✔︎ |
| .some(), .every(), .find() | ✔︎ | ✔︎ |
| Iterator.from() | ✔︎ | xiterable(), new Xiterable() |
| .length, .at() | ❌ | ✔︎ |
| .slice() with negative indexes, .reversed() | ❌ | ✔︎ |
| .findIndex(), .indexOf(), .lastIndexOf(), .includes() | ❌ | ✔︎ |
| .concat(), .flat(), .join(), .reduceRight() | ❌ | ✔︎ |
| .entries(), .keys(), .values() | ❌ | ✔︎ |
| .takeWhile(), .zip() | ❌ | ✔︎ |
| xrange(), repeat(), zip(), zipWith() | ❌ | ✔︎ |
| works on any iterable without wrapping | ❌ (Iterator.from() first) | ✔︎ |
| re-iterable | ❌ | ✔︎ |
| async variant | in progress | ❌ |
Mixing the two
An Xiterable is itself an iterable, so Iterator.from() accepts it whenever you want to hand one to code that expects a built-in iterator. Going the other way, any built-in iterator can be passed to xiterable(). Just remember the one-shot nature of the source carries over: the resulting Xiterable is only good for one pass.
