Functional Weave
Code in TypeScript

collections.chunk

Split a list into consecutive batches of at most n items, in order; the last batch may be short.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 11 tests, run in TypeScript, Python and Rust.

What it does

Splits a list into consecutive batches of `size` items: the shape an API with a "100 records per request" limit, a bulk insert or a mail-merge run needs. Every item appears in exactly one batch, in its original order, and only the last batch may be short.

An empty list gives no batches (`[]`), not one empty batch (`[[]]`), so `for batch in chunk(items, 100)` never sends an empty request.

For example

  • chunk(1, 2, 3, 4, 5, 6, 7, 3) → ×3 seven items in threes leaves a short last batch
  • chunk(1, 2, 3, 4, 5, 6, 3) → ×2 a size that divides exactly leaves no short batch
  • chunk(a, b, c, 1) → ×3 size one gives one batch per item

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function chunk<T>(items: readonly T[], size: number): readonly (readonly T[])[]
itemsT[]the list to split; it is not modified
sizeintthe most items per batch, 1 or greater
returnsT[][]batches in input order; an empty list gives no batches

Your code names it in one line, in the file that uses it

import { chunk } from "#fune/collections.chunk@^1";
impl/typescript.ts · 24 lines · open · raw
/**
 * Split `items` into consecutive batches of at most `size`, in order.
 *
 * An empty list gives no batches rather than one empty batch, so a loop over
 * the result never sends an empty request. A size below 1 throws: it is
 * always a bug upstream, and "no limit" is the outage batching prevents.
 */
export function chunk<T>(items: readonly T[], size: number): readonly (readonly T[])[] {
  if (!Array.isArray(items)) {
    throw new TypeError("chunk needs a list of items");
  }
  if (!Number.isInteger(size)) {
    throw new TypeError(`size must be a whole number, received ${size}`);
  }
  if (size < 1) {
    throw new RangeError(`size must be at least 1, received ${size}`);
  }

  const batches: T[][] = [];
  for (let start = 0; start < items.length; start += size) {
    batches.push(items.slice(start, start + size));
  }
  return batches;
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add collections.chunk
Download for TypeScript collections.chunk-1.0.0-typescript.fune · 4,807 bytes sha256 e22308e6f7fa50e7fafdd374691ef112be6bd177210e489ed75a72ef8b03b8fd

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./collections.chunk-1.0.0-typescript.fune, or fetch it from a terminal with fune pull collections.chunk@1.0.0:typescript.

The whole function, every language, is one file too: collections.chunk-1.0.0.fune, 7,030 bytes, sha256 ac5a3b9d4cf08b46bee335755d23351d37b66124c4e76f71be2bf77ad356cf0e. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before collections.chunk

after — your function gets the result and the arguments, and returns the final result.

// fune: after collections.chunk

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show collections.chunk --steps.

// fune: step collections.chunk after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
seven items in threes leaves a short last batch 1, 2, 3, 4, 5, 6, 7, 3 → ×3
a size that divides exactly leaves no short batch 1, 2, 3, 4, 5, 6, 3 → ×2
size one gives one batch per item a, b, c, 1 → ×3
a size larger than the list is one batch holding everything a, b, 10 → ×1
a size equal to the list is one full batch a, b, c, 3 → ×1
an empty list gives no batches, not one empty batch , 5 →
records pass through untouched and in order items ×3, 2 → ×2
one short of a whole number of batches 1, 2, 3, 4, 5, 2 → ×3
size zero is an error, not 'no limit' 1, 2, 3, 0 → error: size must be at least 1
a negative size is an error 1, 2, 3, -2 → error: size must be at least 1
Show the other 1 test
CaseArgumentsExpected
a fractional size is an error 1, 2, 3, 1.5 → error: size must be a whole number

More from the author

A `size` below 1 is an error rather than "everything in one batch": a zero or negative batch size is always a bug upstream, and quietly sending the whole list at once is the outage the batching was meant to prevent. A fractional size is an error too (TypeScript and Python; Rust's `i64` cannot express one).

A `size` larger than the list gives one batch holding everything.

The batches are new lists; the input is never modified. The items themselves are not copied, so objects in the batches are the same objects as in the input.

Files

PathBytes
README.md947
impl/python.py1,026
impl/rust.rs1,096
impl/typescript.ts858
vectors.json1,378