API reference@evolu/common › Array

Array helpers that help TypeScript help you.

  • Non-empty arrays: compile-time guarantee of at least one element
  • Readonly arrays: prevents accidental mutation

Instead of checking array length at runtime, use NonEmptyReadonlyArray so TypeScript rejects empty arrays at compile time. Functions like firstInArray require a non-empty array — TypeScript won't let us pass an empty one. mapArray preserves non-emptiness (native map doesn't), while appendToArray and prependToArray guarantee the result is non-empty.

All helpers return readonly arrays for safety. Consider how dangerous native sort() is — it mutates the original array and returns it, making bugs hard to track:

const sortScores = (arr: number[]) => arr.sort((a, b) => a - b);

const scores = [3, 1, 2];
const leaderboard = sortScores(scores);
expect(leaderboard).toEqual([1, 2, 3]);
expect(scores).toEqual([1, 2, 3]);
expect(leaderboard).toBe(scores);

Imagine every method doing that.

On a ReadonlyArray, .sort() doesn't even exist. Use sortArray instead:

import { sortArray } from "@evolu/common";

const sortScores = (arr: ReadonlyArray<number>) =>
  sortArray(arr, (a, b) => a - b);

const scores: ReadonlyArray<number> = [3, 1, 2];
const leaderboard = sortScores(scores);
expect(leaderboard).toEqual([1, 2, 3]);
expect(scores).toEqual([3, 1, 2]);
expect(leaderboard).not.toBe(scores);

Even better, require a NonEmptyReadonlyArray — there's nothing to sort if the array is empty anyway:

import { sortArray, type NonEmptyReadonlyArray } from "@evolu/common";

const sortScores = (arr: NonEmptyReadonlyArray<number>) =>
  sortArray(arr, (a, b) => a - b);

const leaderboard = sortScores([3, 1, 2]);
expect(leaderboard).toEqual([1, 2, 3]);
expectTypeOf(leaderboard).toEqualTypeOf<NonEmptyReadonlyArray<number>>();

Sorting an empty array isn't expensive, but functions can have side effects like database queries or network requests. Using non-empty arrays whenever possible is a good convention.

When to use native methods

These helpers only exist where they add type-level value. Native methods like find, some, every, includes, indexOf, and findIndex work well on readonly arrays without mutation — use them directly.

import { type NonEmptyReadonlyArray } from "@evolu/common";

const valid: NonEmptyReadonlyArray<number> = [1, 2, 3];
// @ts-expect-error An empty array is not non-empty.
const invalid: NonEmptyReadonlyArray<number> = [];

expect(valid.find((value) => value === 2)).toBe(2);

Composition

All array helpers use a data-first style (the array is the first argument) because it's natural for single operations:

import { mapArray } from "@evolu/common";

interface Message {
  readonly timestamp: number;
}

const messages: ReadonlyArray<Message> = [{ timestamp: 10 }, { timestamp: 20 }];
const timestamps = mapArray(messages, (m) => m.timestamp);
expect(timestamps).toEqual([10, 20]);

Data-first style also reads well for a few operations, often fitting on a line:

import {
  dedupeArray,
  filterArray,
  firstInArray,
  isNonEmptyArray,
  lastInArray,
  mapArray,
  orderNumber,
  sortArray,
} from "@evolu/common";

const cheapest = firstInArray(sortArray([30, 10, 20], orderNumber));
expect(cheapest).toBe(10);

const users = [{ name: "Ada" }, { name: "Linus" }, { name: "Ada" }];
const uniqueNames = dedupeArray(mapArray(users, (u) => u.name));
expect(uniqueNames).toEqual(["Ada", "Linus"]);

const jobs = [
  { id: 1, done: false },
  { id: 2, done: true },
];
const completedJobs = filterArray(jobs, (job) => job.done);
if (!isNonEmptyArray(completedJobs)) throw new Error("Expected a job");
const latestDone = lastInArray(completedJobs);

expect(latestDone).toEqual({ id: 2, done: true });

For more operations, create a function like getOldestActiveUser or a generic helper.

Some libraries provide dual APIs with data-last for pipe-based composition. Evolu prefers simplicity (in Latin, simplex means "one") so we don't have to choose between seemingly equivalent options (Buridan's ass dilemma).

Evolu doesn't provide pipe because few operations compose well without it, and for more operations, well-named functions communicate intent better.

Types

NameDescription
AtLeastTwoReadonlyArrayA readonly array with at least two elements.
NonEmptyArrayAn array with at least one element.
NonEmptyReadonlyArrayA readonly array with at least one element.
ZipArrayResultExtracts element types from a tuple of arrays, producing a tuple type.
isNonEmptyArrayChecks if an array is non-empty and narrows its type to NonEmptyArray or NonEmptyReadonlyArray based on the input.

Constants

VariableDescription
emptyArrayAn empty readonly array.

Constructors

FunctionDescription
arrayFromBetter Array.from.
arrayFromAsyncBetter Array.fromAsync.

Transformations

FunctionDescription
appendToArrayAppends an item to an array, returning a new non-empty readonly array.
concatArraysConcatenates two arrays, returning a new readonly array.
dedupeArrayReturns a new readonly array with duplicate items removed. Items are compared using Set equality (SameValueZero): primitives by value, and objects and arrays by reference. If by is provided, it derives the comparison key.
filterArrayFilters an array using a predicate or refinement function, returning a new readonly array.
flatMapArrayMaps each element to an array and flattens the result.
mapArrayMaps an array using a mapper function, returning a new readonly array.
partitionArrayPartitions an array into two readonly arrays based on a predicate or refinement function.
prependToArrayPrepends an item to an array, returning a new non-empty readonly array.
reverseArrayReturns a new reversed readonly array.
sortArrayReturns a new sorted readonly array.
spliceArrayReturns a new readonly array with elements removed and/or replaced.
zipArrayCombines multiple arrays into an array of tuples.

Accessors

FunctionDescription
firstInArrayReturns the first element of a non-empty array.
lastInArrayReturns the last element of a non-empty array.