Skip to content

Iterable

The Iterable module operates on any value that implements [Symbol.iterator] — arrays, strings, Sets, Maps, generators, and your own custom lazy sequences. Unlike Array, its transformations are lazy: map, filter, take, drop, and friends return new iterables that do no work until something pulls values out of them. This lets you describe pipelines over unbounded sequences and only pay for the slice you actually materialize.

import { Array, Iterable } from "effect"
// An infinite sequence of naturals — nothing runs yet.
const naturals = Iterable.range(1)
// Compose lazily: still nothing runs.
const evenSquares = Iterable.filter(
Iterable.map(naturals, (n) => n * n),
(n) => n % 2 === 0
)
// Bound it, then materialize. Only 4 squares are ever computed.
const firstFour = Iterable.take(evenSquares, 4)
console.log(Array.fromIterable(firstFour))
// => [4, 16, 36, 64]

Mental model: producers, transformers, consumers

Section titled “Mental model: producers, transformers, consumers”

The whole module falls into three buckets, and knowing which is which tells you when work happens:

  • Producers create iterables, possibly infinite: range, makeBy, repeat, forever, unfold, replicate, of, empty.
  • Transformers take an iterable and return a new lazy iterable: map, filter, flatMap, take, drop, scan, zip, intersperse, chunksOf, group, … No work happens until the result is iterated.
  • Consumers pull values and produce a final answer, doing the work immediately: reduce, size, head, findFirst, some, countBy, forEach, contains, groupBy, and Array.fromIterable / Array.from.
import { Array, Iterable } from "effect"
// `map` is a transformer: this logs nothing.
const mapped = Iterable.map([1, 2, 3], (n) => {
console.log("mapping", n)
return n * 10
})
// `Array.fromIterable` is a consumer: now the logs fire and we get a result.
console.log(Array.fromIterable(mapped))
// => mapping 1
// => mapping 2
// => mapping 3
// => [10, 20, 30]

Iterable vs Array: when to reach for which

Section titled “Iterable vs Array: when to reach for which”

Both modules share most of the same operation names, but they make opposite tradeoffs:

IterableArray
EvaluationLazy — intermediate steps don’t allocateEager — each step allocates a new array
Indexing / lengthNo random access; size walks the whole thingO(1) length, index access, slicing
Infinite sequencesSupported (range, forever, unfold)Not possible
Best forStreaming, fused pipelines, “first N matching”Random access, repeated reads, small finite data

Reach for Iterable when you are chaining several transformations, when the source may be unbounded, or when you only need a prefix of a large computation. Reach for Array when you need indexing, length, or a concrete materialized collection. Convert from one to the other with Array.fromIterable.

import { Array, Iterable } from "effect"
// Lazy: only enough candidates are tested to find the first 3 primes above 100.
const isPrime = (n: number) =>
n > 1 &&
!Iterable.some(Iterable.range(2, Math.floor(Math.sqrt(n))), (d) => n % d === 0)
const bigPrimes = Iterable.filter(Iterable.range(101), isPrime)
console.log(Array.fromIterable(Iterable.take(bigPrimes, 3)))
// => [101, 103, 107]

NonEmptyIterable: a compile-time non-empty guarantee

Section titled “NonEmptyIterable: a compile-time non-empty guarantee”

NonEmptyIterable<A> is an Iterable<A> branded with a type-level proof that it contains at least one element. It is accepted anywhere an Iterable<A> is, but lets callers safely read the head without an Option. Use it for APIs (reductions, aggregations, comparisons) that are undefined for empty input.

import { NonEmptyIterable } from "effect"
function firstOf<A>(data: NonEmptyIterable.NonEmptyIterable<A>): A {
// `unprepend` is safe here because the type guarantees an element exists.
const [first] = NonEmptyIterable.unprepend(data)
return first
}
// A generator typed as NonEmptyIterable satisfies the brand without a cast.
function* oneToThree(): NonEmptyIterable.NonEmptyIterable<number> {
yield 1
yield 2
yield 3
}
console.log(firstOf(oneToThree()))
// => 1

Every example materializes lazy results with Array.fromIterable so the // => comments show concrete values. Functions are grouped by role; remember the producer / transformer / consumer distinction from above.

Builds an iterable by applying a function to consecutive indices starting at 0. Omit length for an infinite sequence.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.makeBy((n) => n * 2, { length: 4 })))
// => [0, 2, 4, 6]

Integers from start, increasing by 1. With end (inclusive) it is finite; without end it is infinite.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.range(1, 4)))
// => [1, 2, 3, 4]
console.log(Array.fromIterable(Iterable.take(Iterable.range(1), 3)))
// => [1, 2, 3]

Repeats a single value n times (n is clamped to at least 1).

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.replicate("a", 3)))
// => ["a", "a", "a"]

Repeats the entire contents of an iterable n times. Lazy — a fresh iterator is taken from the source for each pass, so the source must be re-iterable.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.repeat([1, 2], 3)))
// => [1, 2, 1, 2, 1, 2]

Repeats an iterable without bound. Always pair it with a terminating consumer such as take.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.take(Iterable.forever([1, 2]), 5)))
// => [1, 2, 1, 2, 1]

Turns a record into an iterable of [key, value] entry tuples.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.fromRecord({ a: 1, b: 2 })))
// => [["a", 1], ["b", 2]]

An iterable that yields nothing. Useful as a base case or as the “skip” result of a flatMap.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.empty<number>()))
// => []

Wraps a single value in an iterable.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.of(42)))
// => [42]

Generates values from a seed: f returns Option.some([value, nextSeed]) to continue or Option.none() to stop. The dual of reduce.

import { Array, Iterable, Option } from "effect"
const countdown = Iterable.unfold(3, (n) =>
n > 0 ? Option.some([n, n - 1] as const) : Option.none()
)
console.log(Array.fromIterable(countdown))
// => [3, 2, 1]

Yields one element before the existing elements.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.prepend([2, 3], 1)))
// => [1, 2, 3]

Yields all elements of another iterable before self.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.prependAll([1, 2], ["a", "b"])))
// => ["a", "b", 1, 2]

Yields self, then one trailing element. If self never completes, the element is never reached.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.append([1, 2, 3], 4)))
// => [1, 2, 3, 4]

Concatenates two iterables lazily; that is not touched until self is exhausted.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.appendAll([1, 2], [3, 4])))
// => [1, 2, 3, 4]

Keeps at most the first n elements. The canonical way to bound an infinite iterable.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.take(Iterable.range(1), 3)))
// => [1, 2, 3]

Keeps the longest leading run for which the predicate holds, then stops. Supports type refinements.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.takeWhile([2, 4, 6, 3, 8], (n) => n % 2 === 0)))
// => [2, 4, 6]

Skips the first n elements and yields the rest. Combine with take to slice.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.take(Iterable.drop([1, 2, 3, 4, 5], 1), 3)))
// => [2, 3, 4]

Splits into arrays of length n; the final chunk may be shorter.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.chunksOf([1, 2, 3, 4, 5], 2)))
// => [[1, 2], [3, 4], [5]]

Checks whether the iterable yields no elements by pulling a single value. Acts as a type guard narrowing to Iterable<never>.

import { Iterable } from "effect"
console.log(Iterable.isEmpty([]))
// => true
console.log(Iterable.isEmpty([1]))
// => false

Counts elements by walking the entire iterable. Never call on an unbounded source.

import { Iterable } from "effect"
console.log(Iterable.size([1, 2, 3, 4, 5]))
// => 5

Returns the first element as an Option, None if empty.

import { Iterable } from "effect"
console.log(Iterable.head([1, 2, 3]))
// => { _id: 'Option', _tag: 'Some', value: 1 }
console.log(Iterable.head([]))
// => { _id: 'Option', _tag: 'None' }

Returns the first element directly, throwing on an empty iterable. Use only when you know the iterable is non-empty.

import { Iterable } from "effect"
console.log(Iterable.headUnsafe([1, 2, 3]))
// => 1
// Iterable.headUnsafe([]) // throws Error: "headUnsafe: empty iterable"

Returns the first matching element as an Option. Accepts a boolean predicate, a type refinement, or an Option-returning function (which also transforms the result).

import { Iterable } from "effect"
console.log(Iterable.findFirst([1, 3, 4, 6], (n) => n % 2 === 0))
// => { _id: 'Option', _tag: 'Some', value: 4 }

Like findFirst but returns the last match. Always walks the whole iterable.

import { Iterable } from "effect"
console.log(Iterable.findLast([1, 4, 6, 8, 2], (n) => n % 2 === 0))
// => { _id: 'Option', _tag: 'Some', value: 2 }

Tests membership using Effect’s default Equal equivalence (so it works with value-equal data types, not just primitives).

import { Iterable } from "effect"
console.log(Iterable.contains([1, 2, 3], 2))
// => true
console.log(Iterable.contains([1, 2, 3], 9))
// => false

Builds a contains that uses a custom equivalence function.

import { Iterable } from "effect"
const containsCi = Iterable.containsWith<string>(
(a, b) => a.toLowerCase() === b.toLowerCase()
)
console.log(containsCi(["Hello", "World"], "hello"))
// => true

Transforms each element. The callback receives the index. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.map([1, 2, 3], (n) => n * n)))
// => [1, 4, 9]

Maps each element to an iterable, then concatenates the results. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.flatMap([1, 2, 3], (n) => Iterable.range(1, n))))
// => [1, 1, 2, 1, 2, 3]

Concatenates an iterable of iterables, one level deep. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.flatten([[1, 2], [3, 4], [5]])))
// => [1, 2, 3, 4, 5]

Maps and filters in one pass using a Result: Result.succeed(b) keeps b, Result.failVoid drops the element. Lazy.

import { Array, Iterable, Result } from "effect"
const parsed = Iterable.filterMap(["1", "x", "3"], (s) => {
const n = Number(s)
return Number.isNaN(n) ? Result.failVoid : Result.succeed(n)
})
console.log(Array.fromIterable(parsed))
// => [1, 3]

Like filterMap, but stops at the first Result.failVoid instead of skipping it.

import { Array, Iterable, Result } from "effect"
const upToInvalid = Iterable.filterMapWhile(["1", "2", "x", "4"], (s) => {
const n = Number(s)
return Number.isNaN(n) ? Result.failVoid : Result.succeed(n)
})
console.log(Array.fromIterable(upToInvalid))
// => [1, 2]

Keeps elements matching a predicate; supports type refinements. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.filter([1, 2, 3, 4], (n) => n % 2 === 0)))
// => [2, 4]

Runs a side-effecting callback for each element. A consumer; returns void.

import { Iterable } from "effect"
Iterable.forEach(["a", "b"], (x, i) => console.log(i, x))
// => 0 a
// => 1 b

Folds the iterable left-to-right into a single value. A consumer.

import { Iterable } from "effect"
console.log(Iterable.reduce([1, 2, 3, 4], 0, (acc, n) => acc + n))
// => 10

Like reduce, but yields every intermediate accumulator (starting with the seed). Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.scan([1, 2, 3], 0, (acc, n) => acc + n)))
// => [0, 1, 3, 6]

Returns true if any element matches the predicate. Short-circuits on the first match.

import { Iterable } from "effect"
console.log(Iterable.some([1, 3, 4], (n) => n % 2 === 0))
// => true

Counts how many elements satisfy the predicate. A consumer.

import { Iterable } from "effect"
console.log(Iterable.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0))
// => 2

Keeps only the Some values from an iterable of Options, unwrapping them. Lazy.

import { Array, Iterable, Option } from "effect"
console.log(
Array.fromIterable(
Iterable.getSomes([Option.some(1), Option.none(), Option.some(2)])
)
)
// => [1, 2]

Keeps only the failure values from an iterable of Results. Lazy.

import { Array, Iterable, Result } from "effect"
console.log(
Array.fromIterable(
Iterable.getFailures([Result.succeed(1), Result.fail("err"), Result.succeed(2)])
)
)
// => ["err"]

Keeps only the success values from an iterable of Results. Lazy.

import { Array, Iterable, Result } from "effect"
console.log(
Array.fromIterable(
Iterable.getSuccesses([Result.succeed(1), Result.fail("err"), Result.succeed(2)])
)
)
// => [1, 2]

Maps with a function that may return null / undefined, dropping those results. Handy with APIs like Map.get. Lazy.

import { Array, Iterable } from "effect"
const lookup = new Map([["a", 1], ["b", 2]])
const found = Iterable.flatMapNullishOr(["a", "x", "b"], (k) => lookup.get(k))
console.log(Array.fromIterable(found))
// => [1, 2]

Pairs corresponding elements of two iterables; stops at the shorter one. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.zip([1, 2, 3], ["a", "b"])))
// => [[1, "a"], [2, "b"]]

Like zip, but combines each pair with a function instead of tupling.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.zipWith([1, 2, 3], [10, 20, 30], (a, b) => a + b)))
// => [11, 22, 33]

Every pair [a, b] across the two iterables (the cross product). Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.cartesian([1, 2], ["a", "b"])))
// => [[1, "a"], [1, "b"], [2, "a"], [2, "b"]]

Like cartesian, but combines each cross-product pair with a function.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.cartesianWith([1, 2], [10, 20], (a, b) => a * b)))
// => [10, 20, 20, 40]

Inserts a separator between adjacent elements (none before the first or after the last). Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.intersperse([1, 2, 3], 0)))
// => [1, 0, 2, 0, 3]

Groups runs of consecutive equal elements (default Equal equivalence) into non-empty arrays. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.group([1, 1, 2, 3, 3])))
// => [[1, 1], [2], [3, 3]]

Like group, but with a custom equivalence function.

import { Array, Iterable } from "effect"
const grouped = Iterable.groupWith(
["a", "A", "b"],
(x, y) => x.toLowerCase() === y.toLowerCase()
)
console.log(Array.fromIterable(grouped))
// => [["a", "A"], ["b"]]

Groups all elements (not just consecutive ones) into a record keyed by f. A consumer — it walks the whole iterable.

import { Iterable } from "effect"
console.log(Iterable.groupBy([1, 2, 3, 4], (n) => (n % 2 === 0 ? "even" : "odd")))
// => { odd: [1, 3], even: [2, 4] }

Collapses runs of adjacent equal elements (default Equal equivalence) to a single element. Lazy.

import { Array, Iterable } from "effect"
console.log(Array.fromIterable(Iterable.dedupeAdjacent([1, 1, 2, 2, 3, 1])))
// => [1, 2, 3, 1]

Like dedupeAdjacent, but with a custom equivalence function.

import { Array, Iterable } from "effect"
const deduped = Iterable.dedupeAdjacentWith(
["Hello", "HELLO", "world"],
(a, b) => a.toLowerCase() === b.toLowerCase()
)
console.log(Array.fromIterable(deduped))
// => ["Hello", "world"]

A small module providing the NonEmptyIterable<A> type and the one operation that exploits its guarantee.

An Iterable<A> carrying a type-level brand (readonly [nonEmpty]: A) that proves at least one element exists. It is structurally an Iterable, so it can be passed anywhere one is expected; the brand only matters at compile time.

import { Array, NonEmptyIterable } from "effect"
// A plain iterable known to be non-empty is asserted into the branded type.
const ne = Array.make(1, 2, 3) as unknown as NonEmptyIterable.NonEmptyIterable<number>

Splits a non-empty iterable into its first element and an Iterator over the rest. Safe because the type guarantees a head. Note the second element is a raw iterator (stateful, already advanced past the head).

import { Array, NonEmptyIterable } from "effect"
const ne = Array.make(1, 2, 3) as unknown as NonEmptyIterable.NonEmptyIterable<number>
const [first, rest] = NonEmptyIterable.unprepend(ne)
console.log(first)
// => 1
// Wrap the iterator to consume the remaining elements.
console.log(Array.fromIterable({ [Symbol.iterator]: () => rest }))
// => [2, 3]