Arr — Array Utilities
5.94 kB minified + gzipped
JavaScript arrays feature a rich built-in set of methods, yet building structured data pipelines with native array operations exposes two major sources of bugs and boilerplate:
- Prototype methods are data-first: Because
.map(),.filter(), and.slice()live on the array instance, combining them with standalone functions inside a pipeline requires wrapping every step in an inline arrow function such as(items) => items.map(transform). - Lookups and bounds accesses are unsafe: Indexing an array directly (
items[0]) or calling.find()silently producesundefinedwhen an array is empty or a search misses. TypeScript’s default array indexing does not force you to handle that missing case before accessing properties on the result.
Arr addresses both problems by providing pure, curried, data-last functions over immutable
readonly A[] collections. Every operation that can fail to find an element returns an explicit
Maybe<A> container, and every transformation returns a new array without mutating the input.
Safe Access: Bypassing Undefined
Section titled “Safe Access: Bypassing Undefined”Accessing array boundaries directly in JavaScript can crash your program or introduce silent,
propagating undefined values. Arr provides explicit boundary accessors that return Maybe:
Because these functions return standard Maybe values, you can chain transformations and fallback
defaults linearly without writing defensive if (items.length > 0) guards:
To check array length or emptiness inside a pipeline, Arr.size, Arr.is.empty, and
Arr.is.nonEmpty inspect the collection directly. Furthermore, Arr.is.nonEmpty acts as a
TypeScript type guard that narrows readonly A[] to Arr.NonEmpty<A>:
Searching and Predicates
Section titled “Searching and Predicates”When searching for elements or indices, Arr.findFirst, Arr.findLast, Arr.findIndex, and
Arr.at return Maybe rather than -1 or undefined. Arr.at also supports negative indices to
count backward from the end of the array:
When you need to search an array and transform the matching element at the same time, Arr.findMap
evaluates a Maybe-returning callback on each element and stops as soon as it receives the first
Some:
To test whether any or all elements in a collection satisfy a condition, Arr.some and Arr.every
provide curried boolean predicates that short-circuit as soon as the outcome is decided:
Transformations, Filtering, and Unwrapping
Section titled “Transformations, Filtering, and Unwrapping”Standard array transformations are curried and data-last. Use Arr.map to transform values,
Arr.mapWithIndex when your callback also needs the zero-based element index, Arr.filter to keep
matching elements, and Arr.reverse to produce a reversed copy without mutating the source array:
For nested arrays, Arr.flatMap maps each element to an array and flattens the result by one level,
while Arr.flatten flattens an existing two-dimensional array:
Single-pass mapping and filtering with filterMap and compact
Section titled “Single-pass mapping and filtering with filterMap and compact”Mapping over a collection and discarding invalid items with native array methods requires multiple
iterations (items.map(parse).filter(Boolean)). Arr.filterMap combines mapping and filtering into
a single pass, keeping only the unwrapped Some values and discarding None results:
If you already have an array of Maybe<A> containers, Arr.compact unwraps all Some values and
removes all None entries in one step:
Partitioning and Grouping
Section titled “Partitioning and Grouping”When you need to keep both passing and failing items rather than discarding failures, Arr provides
four specialized partitioning tools:
Arr.partition splits an array using a boolean predicate (or TypeScript type guard) into a tuple of
[passing, failing] arrays. Arr.partitionMaybe applies a Maybe-returning parser and separates
the unparseable raw inputs from the parsed values into [failures, successes]:
When your validation logic returns a Result<E, B> with a specific error message for each rejected
item, Arr.partitionMap maps each item and collects [errors, successes]. If you already hold an
array of Result<E, A> values, Arr.separate splits them directly into [errors, successes]:
To bucket items into a dictionary of non-empty arrays by a computed string key, use Arr.groupBy:
Deduplication and Sorting
Section titled “Deduplication and Sorting”Arr offers three deduplication strategies depending on how equality is defined in your domain:
Arr.uniq removes duplicates across the array using strict equality. Arr.uniqBy deduplicates
objects by comparing a projected key such as an ID or email address. Arr.uniqWith accepts a custom
Equality<A> function for deep or composite comparisons. In all cases, the order of first
occurrence is preserved:
When working with time-series streams where you only want to collapse consecutive duplicate readings
while keeping later repetitions, use Arr.dedupeAdjacent:
For immutable sorting, Arr.sortBy accepts a standard comparator function (a, b) => number, while
Arr.sortWith integrates directly with composable Ordering<A> instances from @pplnd/core:
Slicing and Immutable Modifications
Section titled “Slicing and Immutable Modifications”To extract prefixes or suffixes of an array, Arr.take and Arr.drop slice by element count, while
Arr.takeWhile and Arr.dropWhile slice while a condition holds. Arr.splitAt divides an array at
a target index into a [before, after] tuple in a single call:
To add or remove elements at specific positions without mutating the array in place, Arr.insertAt
and Arr.removeAt return fresh copies. Arr.prepend and Arr.append add an element to the start
or end of an array and statically narrow the return type to Arr.NonEmpty<A>:
Combinations, Folds, and Scans
Section titled “Combinations, Folds, and Scans”When combining two arrays element-by-element, Arr.zip pairs items into [A, B] tuples up to the
length of the shorter array, and Arr.zipWith combines each pair using a custom function.
Arr.concat appends one array onto another, and Arr.intersperse places a separator element
between every item:
To fold an array into a single summary value, use Arr.reduce. When you need to keep every
intermediate accumulator state — such as computing a running account balance across a ledger of
transactions — use Arr.scan:
Chunking, Windowing, and Indexing
Section titled “Chunking, Windowing, and Indexing”When batching API requests or paginating datasets, Arr.chunksOf divides an array into fixed-size
batches. When you instead want to group consecutive elements that share a computed property — such
as adjacent log entries from the same service — use Arr.chunkBy:
For moving averages or trend detection, Arr.windowed produces overlapping sliding windows of a
given size, advancing by step (which defaults to 1):
To build fast lookup maps from an array, Arr.indexBy indexes elements into a ReadonlyMap<K, A>
using a key selector, and Arr.frequencies counts how many times each distinct element appears:
Sequence Generation: unfold
Section titled “Sequence Generation: unfold”When generating an array from an initial seed state until a termination condition is reached — such
as building pagination offsets or time intervals — Arr.unfold repeatedly invokes your step
function as long as it returns Some([nextItem, nextState]):
Traversal across Contexts: traverse and sequence
Section titled “Traversal across Contexts: traverse and sequence”Mapping an array with a function that returns a Maybe, Result, Validation, or Task produces
an array of containers such as readonly Result<E, A>[]. In practice, you almost always want the
inverted structure — a single container holding the collected array (Result<E, readonly A[]>).
Arr.traverse maps and flips the container in a single operation, while Arr.sequence flips an
existing array of containers.
Optional traversal (Maybe)
Section titled “Optional traversal (Maybe)”Arr.traverse.Maybe maps each element to a Maybe and returns Some of all results only if every
element succeeded, short-circuiting to None on the first missing value:
Short-circuiting error traversal (Result)
Section titled “Short-circuiting error traversal (Result)”Arr.traverse.Result and Arr.sequence.Result collect Ok values and short-circuit on the first
Err encountered:
Error-accumulating traversal (Validation)
Section titled “Error-accumulating traversal (Validation)”When validating forms or batch imports where you want to report every failure at once rather than
stopping at the first error, use Arr.traverse.Validation and Arr.sequence.Validation:
Asynchronous traversal (Task)
Section titled “Asynchronous traversal (Task)”Arr.traverse.Task and Arr.sequence.Task execute asynchronous Task computations and collect
their resolved values into Task<readonly B[]>. By default, tasks run in parallel; you can pass
{ concurrency } to Arr.traverse.Task to cap the number of in-flight workers:
Non-Empty Arrays: Arr.NonEmpty
Section titled “Non-Empty Arrays: Arr.NonEmpty”When your domain requires an array with at least one element — such as a list of validation errors
or line items in a confirmed order — Arr.NonEmpty<A> (aliased to readonly [A, ...A[]]) encodes
that guarantee at compile time.
Constructing an array with Arr.prepend, Arr.append, or Arr.NonEmpty.singleton produces an
Arr.NonEmpty<A> directly. For arbitrary runtime arrays, Arr.is.nonEmpty narrows the type in
conditionals, and Arr.NonEmpty.from.array lifts the array into Maybe<Arr.NonEmpty<A>>:
While top-level Arr.map and Arr.reverse return general readonly B[] arrays, the Arr.NonEmpty
namespace provides dedicated transformations that preserve the Arr.NonEmpty guarantee across
pipeline steps, as well as direct head, last, and seedless reduce operations that never need
to wrap their result in Maybe:
For more details on non-empty data modeling across collections, see the Non-Empty Collections Guide.
Pipeline Composition
Section titled “Pipeline Composition”Here is an end-to-end checkout reconciliation pipeline that cleans raw order amounts, filters out unparseable entries, deduplicates records, sorts from highest to lowest value, and extracts the top three orders for priority fulfillment:
Problems it solves
Section titled “Problems it solves”- Pipeline transformation without callback wrappers: In API response formatters and event
processors, transforming arrays with native methods requires verbose arrow wrapper functions
inside
pipechains.Arrprovides data-last combinators (Arr.map,Arr.filterMap,Arr.chunksOf,Arr.chunkBy,Arr.groupBy) that compose cleanly into linear pipelines. - Safe element extraction and out-of-bounds protection (
Arr.head,Arr.last,Arr.at): Native indexing (arr[i]) returnsundefinedat runtime without static safety.Arr.head,Arr.last, andArr.atreturnMaybe<A>, supporting negative offsets and guaranteeing out-of-bounds checks before accessing properties. - Batch chunking and sliding windows (
Arr.chunksOf,Arr.chunkBy,Arr.windowed): When submitting bulk inserts to rate-limited APIs, grouping consecutive identical states, or computing moving averages across time-series metrics,Arr.chunksOf,Arr.chunkBy, andArr.windowedpartition collections without custom chunking algorithms. - Categorization and dual-partitioning (
Arr.groupBy,Arr.partition,Arr.partitionMaybe,Arr.partitionMap,Arr.separate,Arr.frequencies): In UI dashboards and reporting tools, collections can be split by boolean criteria (Arr.partition), segregated by optional or fallible parsers (Arr.partitionMaybe,Arr.partitionMap), organized into non-empty buckets (Arr.groupBy), or counted into frequency maps (Arr.frequencies). - Simultaneous mapping and filtering (
Arr.filterMap,Arr.findMap,Arr.compact): Extracting valid data from dirty datasets (such as parsing strings to numbers and discarding unparseable rows) typically requires separate.map()and.filter()passes.Arr.filterMapandArr.findMapexecute transformation and filtering in a single pass. - Sequence generation from seeds (
Arr.unfold): Constructing lists from iterative algorithms or paginated cursor workflows without imperativewhileloops or mutable array allocations. - Traversing collections of fallible or asynchronous steps (
Arr.traverse,Arr.sequence): When running batch operations (such as validating an array of input records or executing tasks for a list of IDs), standard mapping produces collections of containers likeArray<Result<E, A>>orArray<Task<A>>.Arr.traverseandArr.sequenceinvert the collection into a single container (Result<E, readonly A[]>,Validation<E, readonly A[]>, orTask<readonly A[]>), handling short-circuiting, error accumulation, or bounded concurrency automatically.