Composition utilities
The Composition module is the engine room of this library. Beyond pipe and flow, it provides a collection of small, focused utilities to shape, branch, cache, and adapt functions before they enter a pipeline.
This guide acts as a comprehensive reference for these tools. If you are new to the concepts of piping and flow, start with Thinking in pipelines first.
pipe and flow
Section titled “pipe and flow”pipe and flow are covered in Thinking in pipelines, including the
ten-step limit and how to split a long chain into named phases.
Conditional execution: when and unless
Section titled “Conditional execution: when and unless”Piping raw values through standard functions works well for clean, linear logic. However, real-world applications frequently require optional branches. Rather than forcing you to write noisy inline arrow functions with ternary expressions, Composition provides declarative branching combinators that slot into pipe and flow.
when executes a transformation only when a predicate evaluates to true, whereas unless executes the transformation only when the predicate evaluates to false, returning the original input value unchanged otherwise:
Reshaping objects: struct
Section titled “Reshaping objects: struct”When deriving a structured record from a single input value, mapping wrapper functions can become repetitive.
struct compiles a record of field-level transformer functions into a single transformation that maps the input into a structured object:
Asynchronous pipelines: pipeAsync and flowAsync
Section titled “Asynchronous pipelines: pipeAsync and flowAsync”pipe and flow are strictly synchronous and infallible. If a step returns a Promise, standard pipe does not await it — subsequent steps receive the promise instance itself rather than the resolved value.
When a pipeline includes asynchronous transitions, use the dedicated async runners:
pipeAsync: Evaluates a starting value or promise immediately through a sequence of synchronous or asynchronous steps, returning aPromise.flowAsync: Compiles a sequence of steps into a reusable function that returns aPromise.
Currying functions
Section titled “Currying functions”A curried function accepts its arguments one at a time. curry converts a standard two-argument function into a sequence of single-argument functions:
This is highly useful for preparing multi-argument functions to slot as steps inside pipe or flow. curry is overloaded to support functions of 2, 3, or 4 arguments. Functions with rest or default parameters must pass the intended arity as the second argument; an ambiguous arity throws instead of invoking the function early:
Reversing currying
Section titled “Reversing currying”uncurry is the inverse of curry. It converts a curried function back into a standard multi-argument format. It flattens two stages by default; pass 3 or 4 as the second argument for deeper currying. Returned thunks and tasks are preserved rather than invoked. This is useful when passing curried library helpers to external APIs that expect standard JavaScript functions:
Flipping argument order
Section titled “Flipping argument order”flip reverses the argument order of a curried binary function. Its main purpose is to adapt data-last library functions to a data-first format for use outside of pipelines:
Side effects with tap
Section titled “Side effects with tap”tap executes a function for its side effect (such as logging or performance auditing) and returns the original value unchanged, leaving the pipeline’s behavior undisturbed:
Diagnostic taps
Section titled “Diagnostic taps”For debugging, performance auditing, and background tasks, the module provides several standalone, configuration-based side-effect helpers: tapLog, tapInspect, tapAsync, and tapTime.
Printing values with tapLog
Section titled “Printing values with tapLog”tapLog logs the piped value to the console. You can optionally supply a label to prefix the log output, a custom logger destination, or a custom string formatter (which defaults to JSON.stringify for objects):
Detailed object inspection with tapInspect
Section titled “Detailed object inspection with tapInspect”For deeply nested objects, tapLog might output truncated representations. tapInspect formats objects with infinite depth by default using Node’s util.inspect engine:
Background operations with tapAsync
Section titled “Background operations with tapAsync”To trigger asynchronous operations (like telemetry hits, audits, or database writes) without blocking the pipeline or turning it into an async promise chain, use tapAsync. It runs the promise in the background, intercepts any rejections, and routes them to onError (defaulting to console.error):
The pipeline receives userId immediately; it cannot observe whether the background work succeeds
or fails. onError also receives synchronous throws from the callback and must not itself throw.
Use Task.Result when the caller needs to await and handle the outcome.
Timing steps with tapTime
Section titled “Timing steps with tapTime”tapTime measures the execution duration of a side-effect function. It supports both synchronous and asynchronous operations (for promises, the duration is tracked until the promise resolves or rejects).
The pipeline receives its original value without waiting for asynchronous work. A synchronous throw
propagates after timing is reported, but a promise rejection only ends the measurement; it is not
returned to the pipeline caller. Handle expected async failures inside the timed function, or use an
awaited Task.Result when the caller needs the outcome.
The configuration object requires either a label (to log to the console automatically) or an onFinish callback (to route the typed Duration elsewhere), which are mutually exclusive to avoid conflicts:
Function primitives
Section titled “Function primitives”identity returns its input argument exactly as received. It is commonly used as a fallback callback where a no-op transformation is required:
constant returns a function that always yields the same fixed value, ignoring whatever input is supplied to it:
once ensures a function is only executed on its first invocation, caching and returning that original result for all subsequent calls:
tuple and untuple adapt functions between positional multi-argument signatures and single-tuple argument signatures. Use tuple when a pipeline step yields a [A, B] pair (such as Arr.zip or juxt) that you want to pass into a standard multi-argument function, and untuple when passing a tuple-accepting function to a callback API that supplies separate arguments:
Predicate composition
Section titled “Predicate composition”To negate, combine, or project boolean checks, use the Predicate module. It provides Predicate.not, Predicate.and, Predicate.or, Predicate.using, Predicate.all, and Predicate.any. For full details, see the Predicate guide.
Caching results
Section titled “Caching results”memoize wraps a function, caching the results of repeated calls using the input argument as the cache key:
For complex object keys, you can supply a custom key-generator function to evaluate equivalence structurally, or specify a maxSize parameter to constrain memory usage via least-recently-used (LRU) eviction:
memoizeWeak performs the same caching behavior but stores entries using a WeakMap, ensuring that cache entries are automatically garbage-collected when the key object is no longer referenced in memory:
Fan out and combine
Section titled “Fan out and combine”converge takes a combining function and an array of transformers. It applies a single input value to each transformer independently, passing the collected results to the combiner:
Parallel results with juxt
Section titled “Parallel results with juxt”juxt is the simpler sibling of converge. It applies a single input to an array of transformer functions and returns all outcomes in a typed tuple:
Comparing by projection
Section titled “Comparing by projection”on takes a binary comparator and a projection function. It projects both input arguments before comparing them, removing the need to repeat key lookups in sorting and equality functions: