Thinking in pipelines
Most transformations in software follow a simple, linear path: we take a starting value, apply a sequence of steps to alter it, and obtain a result. The architectural challenge is how to express this flow in a way that is readable, easy to modify, and safe to extend.
The friction of standard patterns
Section titled “The friction of standard patterns”In standard JavaScript, we tend to write transformations in one of two ways. The first is to introduce intermediate variables:
This reads top-to-bottom, but every intermediate variable is noise. Names like trimmed and lower exist only to carry a value to the next line. If you need to reorder, add, or remove a step, you are forced to rename variables to keep the code consistent.
The second approach is to nest the method calls directly:
This eliminates intermediate variables, but it only works when the object you are transforming already has the methods you need. The moment you want to introduce a custom function or use a utility from an external library, the chain breaks. You are forced to break the line apart or write confusing, nested function calls:
This reads inside-out. The execution starts in the middle, moves outward, and forces the developer to jump back and forth to understand the execution order.
Linear transformations with pipe
Section titled “Linear transformations with pipe”pipe solves this by taking a starting value and passing it through a sequence of functions. Each function receives the output of the previous step:
The code now reads top-to-bottom, in exactly the order it executes. There are no throwaway variable names, and modifying the pipeline is as simple as inserting or deleting a line.
TypeScript infers the type of the value at each step. If one of your functions returns a type that the next step does not expect, the compiler will point to the exact step where the mismatch occurs, rather than showing a generic error downstream.
Steps as pure functions
Section titled “Steps as pure functions”Any unary function—a function that accepts a single argument—can be passed as a step in a pipeline. This includes inline lambda functions, named functions, or partially-applied library helpers:
Notice that Maybe.fromNullable is passed directly as a value, without wrapping it in an arrow function.
Furthermore, Maybe.map((u) => u.name) is invoked with only a mapping function. It returns a new function that is waiting for the Maybe value, which pipe supplies. This is made possible by the data-last convention.
Reusable pipelines with flow
Section titled “Reusable pipelines with flow”pipe evaluates immediately, producing a result. flow operates the same way but defers execution—it returns a new, reusable function:
Because toSlug is a standard, single-argument function, we can pass it directly to other functional utilities (like Array.prototype.map) without wrapping it in an arrow function:
The rule of thumb is simple: use pipe when you have a value ready and want to compute a result immediately. Use flow when you want to define and name a reusable transformation pipeline.
The data-last convention
Section titled “The data-last convention”Every function in this library is designed with a data-last signature. The value being operated on is always the final argument.
If the library used data-first signatures, we would be forced to write noisy arrow wrappers for every step in our pipeline:
Because pipelined is built data-last, the configuration is provided first, and the resulting function is ready to receive the data:
This design allows library functions to slot directly into pipe and flow with zero visual overhead.
Side effects with tap
Section titled “Side effects with tap”When you want to inspect a value mid-pipeline—such as logging it or updating an analytics tracker—without altering the data or breaking the chain, you can use tap:
tap intercepts the value, executes your side-effectful callback, and then passes the original value through unchanged to the next step. If you remove the tap lines, the behavioral outcome of the pipeline remains identical.
Keeping steps focused
Section titled “Keeping steps focused”A pipeline is most readable when each step performs a single, focused task. If a step expands into multiple lines of complex logic, it is a signal that you should extract it into a named function:
By extracting the implementation details into a named function, the pipeline itself remains a high-level description of what happens. The named function describes how. Both levels of code become independently readable and testable.