JavaScript arrays come with a full set of built-in methods, but they have two friction points in pipelines: they put the data first (making partial application awkward), and they return undefined when something isn’t found. Arr is a collection of array utilities that address both: data-last functions that slot directly into pipe, and Option wherever something might be absent.
JavaScript’s built-in access functions silently return undefined when an element doesn’t exist. Arr makes the absence explicit:
import { Arr, Option } from "@nlozgachev/pipekit/Core";
import { pipe } from "@nlozgachev/pipekit/Composition";
Arr.head([1, 2, 3]); // Some(1)
Arr.head([]); // None — not undefined
Arr.last([1, 2, 3]); // Some(3)
Arr.last([]); // None
Arr.tail([1, 2, 3]); // Some([2, 3]) — everything after the first
Arr.tail([]); // None
Arr.init([1, 2, 3]); // Some([1, 2]) — everything before the last
Arr.init([]); // None
These compose naturally with Option operations:
pipe(
users,
Arr.head, // Option<User>
Option.map((u) => u.displayName), // Option<string>
Option.getOrElse("No users"),
);
findFirst, findLast, and findIndex all return Option for the same reason — the element might not exist:
pipe([1, 2, 3, 4], Arr.findFirst((n) => n > 2)); // Some(3)
pipe([1, 2, 3, 4], Arr.findLast((n) => n > 2)); // Some(4)
pipe([1, 2, 3, 4], Arr.findIndex((n) => n > 2)); // Some(2)
pipe([1, 2], Arr.findFirst((n) => n > 10)); // None
The core transforms work exactly like their built-in counterparts, but curried for pipe:
pipe([1, 2, 3], Arr.map((n) => n * 2)); // [2, 4, 6]
pipe([1, 2, 3, 4], Arr.filter((n) => n % 2 === 0)); // [2, 4]
pipe([1, 2, 3], Arr.reverse); // [3, 2, 1]
partition splits into two groups — those that pass the predicate and those that don’t:
const [evens, odds] = pipe(
[1, 2, 3, 4, 5],
Arr.partition((n) => n % 2 === 0),
); // [[2, 4], [1, 3, 5]]
groupBy groups elements by a key function, returning a record where each group is a NonEmptyList:
pipe(
["apple", "avocado", "banana", "blueberry"],
Arr.groupBy((s) => s[0]),
); // { a: ["apple", "avocado"], b: ["banana", "blueberry"] }
uniq removes duplicates using strict equality; uniqBy removes duplicates by a key function:
Arr.uniq([1, 2, 2, 3, 1]); // [1, 2, 3]
pipe(
[{ id: 1, name: "a" }, { id: 1, name: "b" }, { id: 2, name: "c" }],
Arr.uniqBy((x) => x.id),
); // [{ id: 1, name: "a" }, { id: 2, name: "c" }]
sortBy sorts without mutating:
pipe([3, 1, 4, 1, 5], Arr.sortBy((a, b) => a - b)); // [1, 1, 3, 4, 5]
flatMap and flatten for working with nested arrays:
pipe([1, 2, 3], Arr.flatMap((n) => [n, n * 10])); // [1, 10, 2, 20, 3, 30]
Arr.flatten([[1, 2], [3], [4, 5]]); // [1, 2, 3, 4, 5]
pipe([1, 2, 3, 4], Arr.take(2)); // [1, 2]
pipe([1, 2, 3, 4], Arr.drop(2)); // [3, 4]
pipe([1, 2, 3, 1], Arr.takeWhile((n) => n < 3)); // [1, 2]
pipe([1, 2, 3, 1], Arr.dropWhile((n) => n < 3)); // [3, 1]
zip pairs elements from two arrays, stopping at the shorter one:
pipe([1, 2, 3], Arr.zip(["a", "b"])); // [[1, "a"], [2, "b"]]
zipWith combines elements with a function:
pipe([1, 2, 3], Arr.zipWith((a, b) => `${a}${b}`, ["a", "b"])); // ["1a", "2b"]
intersperse inserts a separator between every element:
pipe([1, 2, 3], Arr.intersperse(0)); // [1, 0, 2, 0, 3]
chunksOf splits into fixed-size chunks:
pipe([1, 2, 3, 4, 5], Arr.chunksOf(2)); // [[1, 2], [3, 4], [5]]
reduce folds from the left:
pipe([1, 2, 3, 4], Arr.reduce(0, (acc, n) => acc + n)); // 10
pipe([1, 2, 3], Arr.some((n) => n > 2)); // true
pipe([1, 2, 3], Arr.every((n) => n > 0)); // true
Arr.isNonEmpty([]); // false
Arr.isNonEmpty([1, 2]); // true (also narrows to NonEmptyList)
The traverse family maps each element to a typed container and collects the results. They’re useful when you want to run a fallible operation across every element and collect either all successes or the first failure.
traverse — maps to Option, returns None if any element fails:
const parseNum = (s: string): Option<number> => {
const n = Number(s);
return isNaN(n) ? Option.none() : Option.of(n);
};
pipe(["1", "2", "3"], Arr.traverse(parseNum)); // Some([1, 2, 3])
pipe(["1", "x", "3"], Arr.traverse(parseNum)); // None
traverseResult — maps to Result, returns the first Err if any element fails:
const validatePositive = (n: number): Result<string, number> =>
n > 0 ? Result.ok(n) : Result.err("not positive");
pipe([1, 2, 3], Arr.traverseResult(validatePositive)); // Ok([1, 2, 3])
pipe([1, -1, 3], Arr.traverseResult(validatePositive)); // Err("not positive")
traverseTask — maps to Task and runs all in parallel:
pipe(
userIds,
Arr.traverseTask((id) => fetchUser(id)),
)(); // Promise<User[]> — all fetches run in parallel
sequence, sequenceResult, sequenceTask — shorthand for when you already have an array of containers and want to flip Array<Option<A>> into Option<Array<A>>:
Arr.sequence([Option.of(1), Option.of(2)]); // Some([1, 2])
Arr.sequence([Option.of(1), Option.none()]); // None