break/continue controls for loops and higher-order functions (sync, async, concurrent). Designed to compose with ts-pattern.
npm install loop-controlsbreak/continue for loops used together with higher-order functions - sync, async, and bounded-concurrent - designed to compose cleanly with ts-pattern.
Loop controls are not available in the presence of callbacks:
``typescript
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
})
...
}
`
With loop-controls, you can break/continue from within callbacks by using the control object $:
`typescript
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();
});
`
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; in find/findAsync, an optional value becomes the function's return value.forEachConcurrent
- In concurrent functions (), the control also includes signal: AbortSignal to cancel in-flight I/O that respects AbortSignal (e.g., fetch).
- forEach(iter, (item, $, i) => void): { broken: false } | { broken: true }reduce(iter, seed, (acc, item, $, i) => acc): acc
- find(iter, (item, $, i) => boolean): item | undefined
- - calling $.break(value) returns value instead.find(iter, (item, $, i) => item is S): S | undefined
- - supports type-guard predicates.
- forEachAsync(iter, async (item, $, i) => void): Promise<{ broken: false } | { broken: true }>reduceAsync(iter, seed, async (acc, item, $, i) => acc): Promise
- findAsync(iter, async (item, $, i) => boolean): Promise
-
- 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).
- range(end) / range(start, end, step?) - numeric rangesrepeat(value, count?)
- - repeat a valuecount(start?, step?)
- - counting sequence
`ts
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
});
`
- Implemented with sentinel exceptions (_Break, _Continue) caught by loop wrappers - cheap on the non-throw path.break
- /continue are typed as never`, so TypeScript understands control flow.
- No dependencies.
MIT