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);
leaderboard; // [1, 2, 3]
scores; // [1, 2, 3] — original order lost!

Imagine every method doing that.

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

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

const scores: ReadonlyArray<number> = [3, 1, 2];
const leaderboard = sortScores(scores);
leaderboard; // [1, 2, 3]
scores; // [3, 1, 2] — safe!

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

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

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.

For performance-critical cases where mutation is needed, Evolu provides shiftFromArray and popFromArray — but only because they improve type safety by returning a guaranteed T rather than an optional value.

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.

Examples

// Types - compile-time guarantee of at least one element
const _valid: NonEmptyReadonlyArray<number> = [1, 2, 3];
// ts-expect-error - empty array is not a valid NonEmptyReadonlyArray
const _invalid: NonEmptyReadonlyArray<number> = [];

// Type guards
const arr: ReadonlyArray<number> = [1, 2, 3];
if (isNonEmptyArray(arr)) {
  firstInArray(arr);
}

// Transformations
const appended = appendToArray([1, 2, 3], 4); // [1, 2, 3, 4]
const prepended = prependToArray([2, 3], 1); // [1, 2, 3]
const readonly: ReadonlyArray<number> = [1, 2, 3];
const mapped = mapArray(readonly, (x) => x * 2); // [2, 4, 6]
const filtered = filterArray(readonly, (x) => x > 1); // [2, 3]
const deduped = dedupeArray([1, 2, 1, 3, 2]); // [1, 2, 3]
const [evens, odds] = partitionArray([1, 2, 3, 4, 5], (x) => x % 2 === 0);

// Accessors
const first = firstInArray(["a", "b", "c"]); // "a"
const last = lastInArray(["a", "b", "c"]); // "c"

// Mutations
const mutable: NonEmptyArray<number> = [1, 2, 3];
shiftFromArray(mutable); // 1 (guaranteed to exist)
mutable; // [2, 3]

Composition

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

const timestamps = mapArray(messages, (m) => m.timestamp);

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

const cheapest = firstInArray(sortArray(prices, orderNumber));
const uniqueNames = dedupeArray(mapArray(users, (u) => u.name));
const latestDone = lastInArray(filterArray(jobs, isCompletedJob));

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
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.

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. If by is provided, it will be used to derive the key for uniqueness; otherwise values are used directly. Dedupes by reference equality of values (or extracted keys when by is used).
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.

Mutations

FunctionDescription
popFromArrayPops (removes and returns) the last element from a non-empty mutable array.
shiftFromArrayShifts (removes and returns) the first element from a non-empty mutable array.

Constructors

FunctionDescription
arrayFromBetter Array.from.
arrayFromAsyncBetter Array.fromAsync.