Builtins
Barnum provides typed combinators for composing workflows. TypeScript tracks input and output types through the entire pipeline via phantom types.
Every combinator is either a standalone function (imported from barnum) or a postfix method on TypedAction (chained via .method()), or both. The tables below note availability.
Control flowβ
pipe(...actions)β
Sequential composition. The output of each action becomes the input of the next.
function pipe<T1, T2, T3>(
a: Pipeable<T1, T2>,
b: Pipeable<T2, T3>,
): TypedAction<T1, T3>
Up to 10 actions. Zero arguments returns identity; one argument wraps the action.
Postfix: .then(next) chains a single action.
listFiles.then(forEach(processFile)).then(commit)
// equivalent to pipe(listFiles, forEach(processFile), commit)
all(...actions)β
Run multiple actions concurrently on the same input. Collects outputs as a tuple.
function all<T1, A, B, C>(
a: Pipeable<T1, A>,
b: Pipeable<T1, B>,
c: Pipeable<T1, C>,
): TypedAction<T1, [A, B, C]>
Up to 10 actions. Zero arguments returns TypedAction<any, []>.
Postfix: No.
all(analyzeStyle, analyzeLogic, analyzeSecurity)
// Input: string β Output: [StyleReport, LogicReport, SecurityReport]
chain(first, rest)β
Binary chain. Equivalent to pipe(first, rest).
function chain<T1, T2, T3>(
first: Pipeable<T1, T2>,
rest: Pipeable<T2, T3>,
): TypedAction<T1, T3>
Postfix: .then(rest) is the postfix equivalent.
forEach(action)β
Apply an action to each element of an array, concurrently.
function forEach<TIn, TOut>(
action: Pipeable<TIn, TOut>,
): TypedAction<TIn[], TOut[]>
Postfix: Yes β .forEach(action) on an action that outputs an array.
listFiles.forEach(processFile)
// Input: string[] β Output: ProcessResult[]
branch(cases)β
Dispatch on a tagged union's kind field. Each variant maps to a handler that receives the unwrapped value.
function branch<TCases extends Record<string, Action>>(
cases: TCases,
): TypedAction<BranchInput<TCases>, ExtractOutput<TCases[keyof TCases]>>
All case handlers must produce the same output type.
Postfix: Yes β .branch(cases) on an action that outputs a tagged union.
classify.branch({
NeedsRefactor: refactor,
Clean: drop,
})
loop(bodyFn)β
Iterative loop. The body receives recur (restart with new input) and done (break with value). The body must never complete normally β it always calls recur or done.
function loop<TBreak, TIn>(
bodyFn: (
recur: TypedAction<TIn, never>,
done: TypedAction<TBreak, never>,
) => Pipeable<TIn, never>,
): TypedAction<TIn, TBreak>
Postfix: No.
loop((recur, done) =>
pipe(typeCheck, classifyErrors).branch({
HasErrors: pipe(fix, recur),
Clean: done,
})
)
tryCatch(body, recovery)β
Type-level error handling. The body receives a throwError token; firing it routes to the recovery arm. Both arms must return the same type.
function tryCatch<TIn, TOut, TError>(
body: (throwError: TypedAction<TError, never>) => Pipeable<TIn, TOut>,
recovery: Pipeable<TError, TOut>,
): TypedAction<TIn, TOut>
Handles type-level errors only (not exceptions/panics).
Postfix: No.
tryCatch(
(throwError) => pipe(riskyStep, Result.unwrapOr(throwError)),
fallbackStep,
)
race(...actions)β
Run actions concurrently. First to complete wins; others are cancelled.
function race<TIn, TOut>(
...actions: Pipeable<TIn, TOut>[]
): TypedAction<TIn, TOut>
All actions must have identical input and output types.
Postfix: No.
withTimeout(ms, body)β
Race an action against a timer. Returns Result<TOut, void>.
function withTimeout<TIn, TOut>(
ms: Pipeable<TIn, number>,
body: Pipeable<TIn, TOut>,
): TypedAction<TIn, Result<TOut, void>>
Ok(value) if body completes, Err(void) on timeout.
Postfix: No.
withTimeout(constant(30_000), slowStep)
earlyReturn(bodyFn)β
Create a scope with an early exit. The body receives an earlyReturn token. Output type is the union of normal completion and early return.
function earlyReturn<TEarlyReturn, TIn, TOut>(
bodyFn: (
earlyReturn: TypedAction<TEarlyReturn, never>,
) => Pipeable<TIn, TOut>,
): TypedAction<TIn, TEarlyReturn | TOut>
Postfix: No.
recur(bodyFn)β
Restartable scope. The body receives a restart token that re-executes the body from the beginning with new input.
function recur<TIn, TOut>(
bodyFn: (
restart: TypedAction<TIn, never>,
) => Pipeable<TIn, TOut>,
): TypedAction<TIn, TOut>
Postfix: No.
sleep()β
Delay for the number of milliseconds specified by the input. Cancellable during race teardown.
function sleep(): TypedAction<number, void>
Postfix: No.
bind(bindings, body)β
Bind concurrent values as typed references (VarRef). All bindings are evaluated concurrently; the body receives an array of typed references that can be dereferenced anywhere in the pipeline.
function bind<TBindings extends Action[], TOut>(
bindings: [...TBindings],
body: (vars: InferVarRefs<TBindings>) => BodyResult<TOut>,
): TypedAction<ExtractInput<TBindings[number]>, TOut>
Postfix: No.
bind([getConfig, getUser], ([configRef, userRef]) =>
pipe(processWithConfig(configRef), notifyUser(userRef))
)
bindInput(body)β
Capture the pipeline input as a VarRef for later reference deeper in the pipeline.
function bindInput<TIn, TOut>(
body: (input: VarRef<TIn>) => BodyResult<TOut>,
): TypedAction<TIn, TOut>
Sugar for bind([identity], ([input]) => pipe(drop, body(input))).
Postfix: No.
withResource({ create, action, dispose })β
RAII-style resource management. Creates a resource, merges it with the input, runs an action, then disposes.
function withResource<
TIn extends Record<string, unknown>,
TResource extends Record<string, unknown>,
TOut,
>(args: {
create: Pipeable<TIn, TResource>,
action: Pipeable<TResource & TIn, TOut>,
dispose: Pipeable<TResource, unknown>,
}): TypedAction<TIn, TOut>
Postfix: No.
withResource({
create: createWorktree,
action: doWork,
dispose: cleanupWorktree,
})
dropResult(action)β
Run an action for side effects, discard its output. Returns never (terminates the pipeline β typically used before drop or another action).
function dropResult<TInput, TOutput>(
action: Pipeable<TInput, TOutput>,
): TypedAction<TInput, never>
Postfix: No.
Data manipulationβ
constant(value)β
Produce a fixed value, ignoring the pipeline input.
function constant<TValue>(value: TValue): TypedAction<any, TValue>
Postfix: No.
constant("hello")
// Input: anything β Output: "hello"
identityβ
Pass input through unchanged. A value, not a function.
const identity: TypedAction<any, any>
Postfix: No.
dropβ
Discard the pipeline value. Produces never. A value, not a function.
const drop: TypedAction<any, never>
Postfix: Yes β .drop().
sideEffect.drop()
// equivalent to pipe(sideEffect, drop)
tag(kind)β
Wrap the input as a tagged union member: { kind, value: input }.
function tag<
TDef extends Record<string, unknown>,
TKind extends keyof TDef & string,
>(kind: TKind): TypedAction<TDef[TKind], TaggedUnion<TDef>>
Postfix: Yes β .tag(kind).
tag<{ NeedsRefactor: FileInfo; Clean: FileInfo }, "NeedsRefactor">("NeedsRefactor")
// Input: FileInfo β Output: TaggedUnion<{ NeedsRefactor: FileInfo; Clean: FileInfo }>
merge()β
Merge a tuple of objects into a single object via intersection.
function merge<
TObjects extends Record<string, unknown>[],
>(): TypedAction<TObjects, UnionToIntersection<TObjects[number]>>
Postfix: Yes β .merge().
all(getUser, getSettings).merge()
// Output: User & Settings
flatten()β
Flatten a nested array one level.
function flatten<TElement>(): TypedAction<TElement[][], TElement[]>
Postfix: Yes β .flatten().