Skip to content

Chunk

Chunk<A> is an immutable, ordered sequence of values. It behaves like a read-only array — you can map, filter, take, and index into it — but every operation returns a new Chunk rather than mutating the original, and it compares by value (Equal.equals) rather than by reference. Chunks are the backing collection for Streaming: a stream delivers its elements in chunks, so you will see Chunk whenever you pull data out of a Stream.

import { Chunk } from "effect"
// Build a chunk and transform it — each step yields a fresh, immutable Chunk
const evensDoubled = Chunk.make(1, 2, 3, 4, 5, 6).pipe(
Chunk.filter((n) => n % 2 === 0),
Chunk.map((n) => n * 2)
)
// Convert back to a plain array at the boundary of your code
console.log(Chunk.toReadonlyArray(evensDoubled)) // [4, 8, 12]

Each pipe step produces a new Chunk; the original make(1, …, 6) is never changed. When you need a normal array (to return from an API, say), convert with Chunk.toReadonlyArray.

import { Chunk } from "effect"
const empty = Chunk.empty<number>()
const explicit = Chunk.make(1, 2, 3) // from values
const fromArray = Chunk.fromIterable([1, 2, 3]) // from any Iterable
const single = Chunk.of(1) // one element

append / prepend add a single element; appendAll concatenates two chunks. All of them return a new chunk.

import { Chunk } from "effect"
const base = Chunk.make(2, 3)
console.log(Chunk.toReadonlyArray(Chunk.prepend(base, 1))) // [1, 2, 3]
console.log(Chunk.toReadonlyArray(Chunk.append(base, 4))) // [2, 3, 4]
console.log(
Chunk.toReadonlyArray(Chunk.appendAll(base, Chunk.make(4, 5)))
) // [2, 3, 4, 5]

Chunk.get returns an Option for safe indexing; Chunk.head and Chunk.last are also Option-returning because a chunk may be empty. Chunk.size reports the length.

import { Chunk } from "effect"
const chunk = Chunk.make(10, 20, 30)
console.log(Chunk.size(chunk)) // 3
console.log(Chunk.get(chunk, 1)) // { _id: 'Option', _tag: 'Some', value: 20 }
console.log(Chunk.get(chunk, 5)) // { _id: 'Option', _tag: 'None' }
console.log(Chunk.head(chunk)) // { _id: 'Option', _tag: 'Some', value: 10 }

Because out-of-bounds access yields None rather than undefined, you cannot forget to handle the empty/missing case.

The familiar collection operations are all here and all immutable:

import { Chunk } from "effect"
const chunk = Chunk.make(1, 2, 3, 4, 5)
console.log(Chunk.toReadonlyArray(Chunk.take(chunk, 2))) // [1, 2]
console.log(Chunk.toReadonlyArray(Chunk.drop(chunk, 2))) // [3, 4, 5]
console.log(Chunk.toReadonlyArray(Chunk.reverse(chunk))) // [5, 4, 3, 2, 1]
console.log(Chunk.reduce(chunk, 0, (sum, n) => sum + n)) // 15

Two chunks built separately are equal when their elements are equal, so a Chunk is safe to compare in tests and to use as a value inside other structurally-compared types.

import { Chunk, Equal } from "effect"
console.log(Equal.equals(Chunk.make(1, 2, 3), Chunk.make(1, 2, 3))) // true
console.log(Equal.equals(Chunk.make(1, 2), Chunk.make(1, 2, 3))) // false

For everyday in-memory lists, a plain ReadonlyArray and the Array module are perfectly fine. Reach for Chunk when you are working with Streaming (where it is the native element container), or when you want guaranteed immutability and structural equality for a sequence. Convert with Chunk.fromIterable going in and Chunk.toReadonlyArray coming out.

A Chunk gives you two things a plain array does not:

  • Value equality via Equal. Arrays compare by reference ([1,2] !== [1,2]), while Equal.equals(Chunk.make(1, 2), Chunk.make(1, 2)) is true. This makes chunks safe keys/values inside HashMap, HashSet, and other structurally-compared types, and removes a class of test flakiness.
  • Fast concatenation, append, and prepend. Internally a Chunk is a balanced tree of array segments, so appendAll, append, and prepend are cheap and share structure rather than copying. This is exactly why Stream accumulates and hands you data as chunks: building up output by concatenation is the hot path in streaming.

When you only need an in-memory list and don’t care about either property, the Array module on plain ReadonlyArray is lighter weight.

NonEmptyChunk<A> is a Chunk<A> that the type system knows has at least one element. Constructors like make, of, makeBy, and range return one, and the isNonEmpty refinement narrows a Chunk to a NonEmptyChunk so that “head” and “last” become total (non-Option) operations.

import { Chunk } from "effect"
const chunk: Chunk.Chunk<number> = Chunk.make(1, 2, 3)
if (Chunk.isNonEmpty(chunk)) {
// inside this branch `chunk` is a NonEmptyChunk
console.log(Chunk.headNonEmpty(chunk)) // 1 (a plain number, not Option)
console.log(Chunk.lastNonEmpty(chunk)) // 3
}

Operations that preserve non-emptiness do so in their types: map, reverse, append, prepend, and appendAll/prependAll (when one side is non-empty) all return NonEmptyChunk from a NonEmptyChunk input.


Every public Chunk export, grouped by purpose. All examples assume import { Chunk } from "effect" (plus any extra modules shown). Many operations return a new Chunk; we materialize results with Chunk.toReadonlyArray (// =>) where the printed form matters.

The core immutable sequence type, Chunk<A>. It is Iterable, Equal, Pipeable, and Inspectable, and exposes a .length.

const chunk: Chunk.Chunk<number> = Chunk.make(1, 2, 3)
console.log(chunk.length) // => 3

A Chunk<A> proven to hold at least one element; enables total head/last access.

const ne: Chunk.NonEmptyChunk<number> = Chunk.make(1, 2, 3)
console.log(Chunk.headNonEmpty(ne)) // => 1

The higher-kinded type lambda for Chunk, used when writing generic abstractions over type constructors (HKT).

import type { Chunk, HKT } from "effect"
type NumberChunk = HKT.Kind<Chunk.ChunkTypeLambda, never, never, never, number>
// => Chunk<number>

Type guard: checks whether an unknown value is a Chunk.

console.log(Chunk.isChunk(Chunk.make(1, 2, 3))) // => true
console.log(Chunk.isChunk([1, 2, 3])) // => false

Returns true when the chunk has no elements.

console.log(Chunk.isEmpty(Chunk.empty())) // => true
console.log(Chunk.isEmpty(Chunk.make(1))) // => false

Refinement: returns true when the chunk has at least one element and narrows it to NonEmptyChunk.

const chunk = Chunk.make(1, 2)
if (Chunk.isNonEmpty(chunk)) {
console.log(Chunk.headNonEmpty(chunk)) // => 1
}

Builds an Equivalence for chunks from an element-level Equivalence (compares length, then elements pairwise).

import { Chunk, Equivalence } from "effect"
const eq = Chunk.makeEquivalence(Equivalence.strictEqual<number>())
console.log(eq(Chunk.make(1, 2, 3), Chunk.make(1, 2, 3))) // => true
console.log(eq(Chunk.make(1, 2, 3), Chunk.make(1, 2, 4))) // => false

Creates an empty chunk.

console.log(Chunk.size(Chunk.empty<number>())) // => 0

Builds a NonEmptyChunk from one or more inline values.

console.log(Chunk.toReadonlyArray(Chunk.make(1, 2, 3))) // => [1, 2, 3]

Builds a single-element NonEmptyChunk.

console.log(Chunk.toReadonlyArray(Chunk.of("hello"))) // => ["hello"]

Creates a chunk from any iterable (arrays, sets, generators, …). Returns the input unchanged if it is already a Chunk.

console.log(Chunk.toReadonlyArray(Chunk.fromIterable(new Set([1, 2, 2, 3])))) // => [1, 2, 3]

Wraps an array into a chunk without copying — mutating the source array later mutates the chunk. Use only when you own the array and won’t mutate it.

const arr = [1, 2, 3]
const chunk = Chunk.fromArrayUnsafe(arr)
arr[0] = 999
console.log(Chunk.toReadonlyArray(chunk)) // => [999, 2, 3]

Like fromArrayUnsafe, but for a non-empty array; returns a NonEmptyChunk without copying.

import { Array, Chunk } from "effect"
const chunk = Chunk.fromNonEmptyArrayUnsafe(Array.make(1, 2, 3))
console.log(Chunk.isNonEmpty(chunk)) // => true

Builds a NonEmptyChunk of length n, initializing each element from its index.

console.log(Chunk.toReadonlyArray(Chunk.makeBy(5, (i) => i * 2))) // => [0, 2, 4, 6, 8]

Creates a NonEmptyChunk of consecutive integers from start to end inclusive. If start > end it returns a single-element chunk of start.

console.log(Chunk.toReadonlyArray(Chunk.range(1, 5))) // => [1, 2, 3, 4, 5]

Converts a chunk to a fresh, mutable Array. A NonEmptyChunk yields a NonEmptyArray.

const arr = Chunk.toArray(Chunk.make(1, 2, 3))
console.log(arr) // => [1, 2, 3]
console.log(Array.isArray(arr)) // => true

Converts a chunk to a ReadonlyArray (the canonical way to leave Chunk-land). A NonEmptyChunk yields a NonEmptyReadonlyArray.

console.log(Chunk.toReadonlyArray(Chunk.make(1, 2, 3))) // => [1, 2, 3]

Safely reads the element at an index, returning Option. Out-of-bounds yields None.

import { Chunk, Option } from "effect"
const chunk = Chunk.make("a", "b", "c")
console.log(Chunk.get(chunk, 1)) // => Option.some("b")
console.log(Chunk.get(chunk, 10)) // => Option.none()

Reads the element at an index, throwing on out-of-bounds. Prefer get when the index might be invalid.

console.log(Chunk.getUnsafe(Chunk.make("a", "b", "c"), 2)) // => "c"

Returns the first element as an Option.

console.log(Chunk.head(Chunk.make(1, 2, 3))) // => Option.some(1)
console.log(Chunk.head(Chunk.empty())) // => Option.none()

Returns the first element, throwing if the chunk is empty.

console.log(Chunk.headUnsafe(Chunk.make(1, 2, 3))) // => 1

Total first-element access for a NonEmptyChunk (no Option, never throws).

console.log(Chunk.headNonEmpty(Chunk.make(1, 2, 3))) // => 1

Returns the last element as an Option.

console.log(Chunk.last(Chunk.make(1, 2, 3))) // => Option.some(3)
console.log(Chunk.last(Chunk.empty())) // => Option.none()

Returns the last element, throwing if the chunk is empty.

console.log(Chunk.lastUnsafe(Chunk.make(1, 2, 3))) // => 3

Total last-element access for a NonEmptyChunk.

console.log(Chunk.lastNonEmpty(Chunk.make(1, 2, 3))) // => 3

Returns every element after the first, wrapped in Option (None if empty).

import { Chunk, Option } from "effect"
const t = Chunk.tail(Chunk.make(1, 2, 3))
console.log(Option.map(t, Chunk.toReadonlyArray)) // => Option.some([2, 3])
console.log(Chunk.tail(Chunk.empty())) // => Option.none()

Returns every element after the first from a NonEmptyChunk (total; may be empty).

console.log(Chunk.toReadonlyArray(Chunk.tailNonEmpty(Chunk.make(1, 2, 3)))) // => [2, 3]

Returns the number of elements.

console.log(Chunk.size(Chunk.make(1, 2, 3))) // => 3

Adds one element to the end, returning a NonEmptyChunk.

console.log(Chunk.toReadonlyArray(Chunk.append(Chunk.make(1, 2), 3))) // => [1, 2, 3]

Adds one element to the front, returning a NonEmptyChunk.

console.log(Chunk.toReadonlyArray(Chunk.prepend(Chunk.make(2, 3), 1))) // => [1, 2, 3]

Concatenates two chunks (self first). Non-emptiness of either input is preserved in the type.

console.log(
Chunk.toReadonlyArray(Chunk.appendAll(Chunk.make(1, 2), Chunk.make(3, 4)))
) // => [1, 2, 3, 4]

Concatenates that before self (the prefix comes first).

console.log(
Chunk.make(1, 2).pipe(Chunk.prependAll(Chunk.make("a", "b")), Chunk.toReadonlyArray)
) // => ["a", "b", 1, 2]

Keeps the first n elements.

console.log(Chunk.toReadonlyArray(Chunk.take(Chunk.make(1, 2, 3, 4, 5), 3))) // => [1, 2, 3]

Keeps the last n elements.

console.log(Chunk.toReadonlyArray(Chunk.takeRight(Chunk.make(1, 2, 3, 4, 5), 2))) // => [4, 5]

Keeps the leading run of elements that satisfy a predicate. Accepts a refinement to narrow the element type.

console.log(
Chunk.toReadonlyArray(Chunk.takeWhile(Chunk.make(1, 2, 3, 4, 1), (n) => n < 3))
) // => [1, 2]

Discards the first n elements.

console.log(Chunk.toReadonlyArray(Chunk.drop(Chunk.make(1, 2, 3, 4, 5), 2))) // => [3, 4, 5]

Discards the last n elements.

console.log(Chunk.toReadonlyArray(Chunk.dropRight(Chunk.make(1, 2, 3, 4, 5), 2))) // => [1, 2, 3]

Discards the leading run of elements that satisfy a predicate.

console.log(
Chunk.toReadonlyArray(Chunk.dropWhile(Chunk.make(1, 2, 3, 4, 5), (n) => n < 3))
) // => [3, 4, 5]

Splits into [before, from] at an index.

const [before, after] = Chunk.splitAt(Chunk.make(1, 2, 3, 4), 2)
console.log(Chunk.toReadonlyArray(before)) // => [1, 2]
console.log(Chunk.toReadonlyArray(after)) // => [3, 4]

Splits a NonEmptyChunk at n, returning a non-empty prefix and a possibly-empty suffix (n is clamped to at least 1).

const [first, rest] = Chunk.splitNonEmptyAt(Chunk.make(1, 2, 3, 4), 1)
console.log(Chunk.toReadonlyArray(first)) // => [1]
console.log(Chunk.toReadonlyArray(rest)) // => [2, 3, 4]

Splits into up to n roughly-equal chunks, distributing elements in order.

console.log(
Chunk.toReadonlyArray(Chunk.split(Chunk.make(1, 2, 3, 4, 5), 2)).map(Chunk.toReadonlyArray)
) // => [[1, 2, 3], [4, 5]]

Splits on the first element matching a predicate into [before, fromMatch].

const [before, fromMatch] = Chunk.splitWhere(Chunk.make(1, 2, 3, 4), (n) => n > 2)
console.log(Chunk.toReadonlyArray(before)) // => [1, 2]
console.log(Chunk.toReadonlyArray(fromMatch)) // => [3, 4]

Groups elements into sub-chunks of up to n elements each.

console.log(
Chunk.toReadonlyArray(Chunk.chunksOf(Chunk.make(1, 2, 3, 4, 5), 2)).map(Chunk.toReadonlyArray)
) // => [[1, 2], [3, 4], [5]]

Checks membership using the default Equal equivalence.

console.log(Chunk.contains(Chunk.make(1, 2, 3), 2)) // => true
console.log(Chunk.contains(Chunk.make(1, 2, 3), 9)) // => false

Checks membership using a custom equivalence function.

const ci = Chunk.containsWith<string>((a, b) => a.toLowerCase() === b.toLowerCase())
console.log(ci(Chunk.make("Apple", "Banana"), "apple")) // => true

Returns the first element matching a predicate (or refinement) as an Option.

console.log(Chunk.findFirst(Chunk.make(1, 2, 3, 4), (n) => n > 2)) // => Option.some(3)

Returns the index of the first matching element as an Option.

console.log(Chunk.findFirstIndex(Chunk.make(1, 2, 3, 4), (n) => n > 2)) // => Option.some(2)

Returns the last element matching a predicate (or refinement) as an Option.

console.log(Chunk.findLast(Chunk.make(1, 2, 3, 4), (n) => n < 4)) // => Option.some(3)

Returns the index of the last matching element as an Option.

console.log(Chunk.findLastIndex(Chunk.make(1, 2, 3, 4), (n) => n < 4)) // => Option.some(2)

Returns true if every element satisfies the predicate; with a refinement it narrows the chunk’s element type.

console.log(Chunk.every(Chunk.make(2, 4, 6), (n) => n % 2 === 0)) // => true

Returns true if at least one element satisfies the predicate; narrows the chunk to NonEmptyChunk on success.

console.log(Chunk.some(Chunk.make(1, 2, 3), (n) => n > 2)) // => true

Deletes the element at an index. Out-of-bounds indices leave the chunk unchanged.

console.log(Chunk.toReadonlyArray(Chunk.remove(Chunk.make("a", "b", "c"), 1))) // => ["a", "c"]

Applies a function to the element at an index, returning Option<Chunk> (None if out of bounds).

import { Chunk, Option } from "effect"
const result = Chunk.modify(Chunk.make(1, 2, 3), 1, (n) => n * 10)
console.log(Option.map(result, Chunk.toReadonlyArray)) // => Option.some([1, 20, 3])

Replaces the element at an index with a value, returning Option<Chunk> (None if out of bounds).

import { Chunk, Option } from "effect"
const result = Chunk.replace(Chunk.make("a", "b", "c"), 1, "X")
console.log(Option.map(result, Chunk.toReadonlyArray)) // => Option.some(["a", "X", "c"])

Reverses the order of elements. A NonEmptyChunk stays non-empty.

console.log(Chunk.toReadonlyArray(Chunk.reverse(Chunk.make(1, 2, 3)))) // => [3, 2, 1]

Transforms every element. The mapper receives (element, index); non-emptiness is preserved.

console.log(Chunk.toReadonlyArray(Chunk.map(Chunk.make(1, 2, 3), (n) => n + 1))) // => [2, 3, 4]

Maps statefully, threading an accumulator and returning [finalState, chunk].

const [total, running] = Chunk.mapAccum(Chunk.make(1, 2, 3), 0, (acc, n) => [acc + n, acc + n])
console.log(total) // => 6
console.log(Chunk.toReadonlyArray(running)) // => [1, 3, 6]

Maps each element to a chunk and concatenates the results. The mapper receives (element, index).

console.log(
Chunk.toReadonlyArray(Chunk.flatMap(Chunk.make(1, 2, 3), (n) => Chunk.make(n, n)))
) // => [1, 1, 2, 2, 3, 3]

Flattens a chunk of chunks into a single chunk.

console.log(
Chunk.toReadonlyArray(Chunk.flatten(Chunk.make(Chunk.make(1, 2), Chunk.make(3, 4))))
) // => [1, 2, 3, 4]

Runs a side-effecting function over each element; returns void.

Chunk.forEach(Chunk.make(1, 2, 3), (n) => console.log(n))
// logs 1, 2, 3

Keeps elements satisfying a predicate; with a refinement it narrows the element type.

console.log(
Chunk.toReadonlyArray(Chunk.filter(Chunk.make(1, 2, 3, 4), (n) => n % 2 === 0))
) // => [2, 4]

Maps and filters in one pass: the mapper returns a Result, and only success values are kept. The mapper receives (element, index).

import { Chunk, Result } from "effect"
const nums = Chunk.filterMap(Chunk.make("1", "x", "3"), (s) => {
const n = parseInt(s)
return isNaN(n) ? Result.failVoid : Result.succeed(n)
})
console.log(Chunk.toReadonlyArray(nums)) // => [1, 3]

Like filterMap, but stops at the first element the Filter rejects.

import { Chunk, Result } from "effect"
const result = Chunk.filterMapWhile(Chunk.make("1", "2", "x", "3"), (s) => {
const n = parseInt(s)
return isNaN(n) ? Result.failVoid : Result.succeed(n)
})
console.log(Chunk.toReadonlyArray(result)) // => [1, 2] (stops at "x")

Drops Nones from a Chunk<Option<A>>, keeping the unwrapped Some values.

import { Chunk, Option } from "effect"
console.log(
Chunk.toReadonlyArray(Chunk.compact(Chunk.make(Option.some(1), Option.none(), Option.some(3))))
) // => [1, 3]

Splits a chunk with a Result-returning function into [excluded, satisfying]; the mapper receives (element, index).

import { Chunk, Result } from "effect"
const [excluded, satisfying] = Chunk.partition(Chunk.make(1, -2, 3), (n) =>
n > 0 ? Result.succeed(n) : Result.fail(`neg:${n}`)
)
console.log(Chunk.toReadonlyArray(excluded)) // => ["neg:-2"]
console.log(Chunk.toReadonlyArray(satisfying)) // => [1, 3]

Splits a Chunk<Result<B, A>> into [failures, successes].

import { Chunk, Result } from "effect"
const [errors, values] = Chunk.separate(
Chunk.make(Result.succeed(1), Result.fail("e"), Result.succeed(2))
)
console.log(Chunk.toReadonlyArray(errors)) // => ["e"]
console.log(Chunk.toReadonlyArray(values)) // => [1, 2]

Folds the elements left-to-right with an accumulator; the reducer receives (acc, element, index).

console.log(Chunk.reduce(Chunk.make(1, 2, 3, 4), 0, (acc, n) => acc + n)) // => 10

Folds the elements right-to-left.

console.log(Chunk.reduceRight(Chunk.make(1, 2, 3, 4), 0, (acc, n) => acc + n)) // => 10

Joins a Chunk<string> into a single string with a separator.

console.log(Chunk.join(Chunk.make("a", "b", "c"), ", ")) // => "a, b, c"

Sorts elements with an Order, returning a new chunk.

import { Chunk, Order } from "effect"
console.log(Chunk.toReadonlyArray(Chunk.sort(Chunk.make(3, 1, 2), Order.Number))) // => [1, 2, 3]

Sorts by a value derived from each element via a projection plus an Order.

import { Chunk, Order } from "effect"
const people = Chunk.make({ name: "Bob", age: 25 }, { name: "Alice", age: 30 })
const byAge = Chunk.sortWith(people, (p) => p.age, Order.Number)
console.log(Chunk.toReadonlyArray(byAge).map((p) => p.name)) // => ["Bob", "Alice"]

Concatenates two chunks and removes duplicates, preserving order of first appearance.

console.log(
Chunk.toReadonlyArray(Chunk.union(Chunk.make(1, 2, 3), Chunk.make(3, 4, 5)))
) // => [1, 2, 3, 4, 5]

Keeps values present in both chunks; order and references come from the first.

console.log(
Chunk.toReadonlyArray(Chunk.intersection(Chunk.make(1, 2, 3, 4), Chunk.make(3, 4, 5)))
) // => [3, 4]

Keeps values from self that are not in that.

console.log(
Chunk.toReadonlyArray(Chunk.difference(Chunk.make(1, 2, 3, 4, 5), Chunk.make(3, 4, 6)))
) // => [1, 2, 5]

Like difference, but using a custom equivalence function.

const byId = Chunk.differenceWith<{ id: number }>((a, b) => a.id === b.id)
const result = byId(Chunk.make({ id: 1 }, { id: 2 }), Chunk.make({ id: 1 }))
console.log(Chunk.toReadonlyArray(result)) // => [{ id: 2 }]

Pairs elements pointwise into tuples, stopping at the shorter chunk.

console.log(
Chunk.toReadonlyArray(Chunk.zip(Chunk.make(1, 2, 3), Chunk.make("a", "b", "c")))
) // => [[1, "a"], [2, "b"], [3, "c"]]

Combines elements pointwise with a function, stopping at the shorter chunk.

console.log(
Chunk.toReadonlyArray(
Chunk.zipWith(Chunk.make(1, 2, 3), Chunk.make("a", "b", "c"), (n, l) => `${n}-${l}`)
)
) // => ["1-a", "2-b", "3-c"]

The inverse of zip: splits a chunk of pairs into a pair of chunks.

const [nums, letters] = Chunk.unzip(Chunk.make([1, "a"] as const, [2, "b"] as const))
console.log(Chunk.toReadonlyArray(nums)) // => [1, 2]
console.log(Chunk.toReadonlyArray(letters)) // => ["a", "b"]

Removes duplicate values, keeping the first occurrence of each.

console.log(Chunk.toReadonlyArray(Chunk.dedupe(Chunk.make(1, 2, 2, 3, 1)))) // => [1, 2, 3]

Removes only consecutive duplicate values, leaving non-adjacent repeats intact.

console.log(
Chunk.toReadonlyArray(Chunk.dedupeAdjacent(Chunk.make(1, 1, 2, 2, 3, 1, 1)))
) // => [1, 2, 3, 1]