Skip to content

Arr — array utilities

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 Maybe 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 { pipe } from "@nlozgachev/pipelined/composition";
import { Arr, Maybe } from "@nlozgachev/pipelined/core";

Arr.head([1, 2, 3]); // Some(1)
Arr.head([]); // None

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 Maybe operations:

pipe(
	users,
	Arr.head, // Maybe<User>
	Maybe.map((u) => u.displayName), // Maybe<string>
	Maybe.getOrElse(() => "No users"),
);

findFirst, findLast, and findIndex all return Maybe 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]

insertAt returns a new array with an item inserted before the element at a given index. Negative indices are clamped to 0; indices beyond the array length append to the end:

pipe([1, 2, 3], Arr.insertAt(1, 99)); // [1, 99, 2, 3]
pipe([1, 2, 3], Arr.insertAt(0, 99)); // [99, 1, 2, 3]
pipe([1, 2, 3], Arr.insertAt(3, 99)); // [1, 2, 3, 99]

removeAt returns a new array with the element at the given index removed. If the index is out of bounds the original array is returned unchanged:

pipe([1, 2, 3], Arr.removeAt(1)); // [1, 3]
pipe([1, 2, 3], Arr.removeAt(5)); // [1, 2, 3]

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. For most cases, traverseResult is what you want — it stops at the first failure and tells you what went wrong. Use traverse when working with Maybe; use traverseTask when the operations are independent and can run in parallel.

traverse — maps to Maybe, returns None if any element fails:

const parseNum = (s: string): Maybe<number> => {
	const n = Number(s);
	return isNaN(n) ? Maybe.none() : Maybe.some(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. Note: if you need sequential processing that stops at the first error, use traverseTaskResult instead:

pipe(
	userIds,
	Arr.traverseTask((id) => fetchUser(id)),
)(); // Promise<User[]> — all fetches run in parallel

traverseTaskResult — maps to TaskResult and runs sequentially, short-circuiting on the first Err:

const validate = (id: string): TaskResult<string, User> =>
	TaskResult.tryCatch(
		() => fetch(`/users/${id}`).then((r) => r.json()),
		(e) => `Failed to load ${id}: ${e}`,
	);

pipe(
	["u1", "u2", "u3"],
	Arr.traverseTaskResult(validate),
)(); // TaskResult<string, User[]> — stops at the first failure

sequence, sequenceResult, sequenceTask, sequenceTaskResult — shorthand for when you already have an array of containers and want to flip Array<Maybe<A>> into Maybe<Array<A>>:

Arr.sequence([Maybe.some(1), Maybe.some(2)]); // Some([1, 2])
Arr.sequence([Maybe.some(1), Maybe.none()]); // None

Use Arr when:

  • You’re composing array operations inside a pipe chain and want to avoid data-first method calls
  • You need Maybe-returning access functions (head, last, findFirst) instead of undefined
  • You’re mapping or filtering and want the step to read as a named operation rather than an inline arrow function
  • You need to traverse an array with a fallible or async operation and collect the results

Keep using the native array methods when:

  • The operation is a one-liner in a function body where .map() or .filter() is already clear
  • You don’t need the result to compose in a pipeline