loop-controls
v1.1.0
Published
break/continue controls for loops and higher-order functions (sync, async, concurrent). Designed to compose with ts-pattern.
Maintainers
Readme
loop-controls
break/continue for loops used together with higher-order functions - sync, async, and bounded-concurrent - designed to compose cleanly with ts-pattern.
Motivation
Problem
Loop controls are not available in the presence of callbacks:
import { match } from "ts-pattern";
type Item = { type: "this" } | { type: "that" }
for (const item of items) {
match(item)
.with({ type: "this"}, () => {
// no way to continue or break the loop from here
})
...
}Solution
With loop-controls, you can break/continue from within callbacks by using the control object $:
import { forEach } from "loop-controls";
import { match } from "ts-pattern";
type Item = { type: "this" } | { type: "that" };
forEach(items, (item, $, i) => {
match(item)
.with({ type: "this"}, () => {
$.continue(); // skip to next iteration
})
.with({ type: "that"}, () => {
$.break(); // exit the loop entirely
})
.exhaustive();
});API
All handlers receive a control object $ with methods:
$.continue(): never— skip to the next iteration.$.break(value?: any): never— stop the loop. In reducers, an optional value replaces the accumulator; infind/findAsync, an optional value becomes the function's return value.- In concurrent functions (
forEachConcurrent), the control also includessignal: AbortSignalto cancel in-flight I/O that respectsAbortSignal(e.g.,fetch).
Sync
forEach(iter, (item, $, i) => void): { broken: false } | { broken: true }reduce(iter, seed, (acc, item, $, i) => acc): accfind(iter, (item, $, i) => boolean): item | undefined- calling$.break(value)returnsvalueinstead.find(iter, (item, $, i) => item is S): S | undefined- supports type-guard predicates.
Async (sequential)
forEachAsync(iter, async (item, $, i) => void): Promise<{ broken: false } | { broken: true }>reduceAsync(iter, seed, async (acc, item, $, i) => acc): Promise<acc>findAsync(iter, async (item, $, i) => boolean): Promise<item | undefined>
Async (bounded concurrent over arrays)
forEachConcurrent(items: T[], async (item, $, i) => void, { concurrency: number; } = {}): Promise<{ broken: boolean }>
$.break() cancels the remaining queue and provides $.signal to cancel in-flight I/O that respects AbortSignal (e.g., fetch).
Iterable utilities
range(end)/range(start, end, step?)- numeric rangesrepeat(value, count?)- repeat a valuecount(start?, step?)- counting sequence
import { forEach, range } from "loop-controls";
// Traditional: for (let i = 0; i < 10; i++)
forEach(range(10), (i, $) => {
if (i === 5) $.break();
console.log(i); // 0, 1, 2, 3, 4
});
// Traditional: for (let i = 2; i < 20; i += 3)
forEach(range(2, 20, 3), (i, $) => {
console.log(i); // 2, 5, 8, 11, 14, 17
});Design notes
- Implemented with sentinel exceptions (
_Break,_Continue) caught by loop wrappers - cheap on the non-throw path. break/continueare typed asnever, so TypeScript understands control flow.- No dependencies.
License
MIT
