Designing with states
TypeScript is an exceptional language for describing the shape of data, but it leaves a notable gap when it comes to describing the states our applications are actually in.
We are provided with excellent low-level primitives: null and undefined for absence, try/catch blocks for errors, and Promise for asynchronous operations. These are mechanisms, not models. Because the language does not provide first-class structures to carry the semantic meaning of these states, every project is forced to invent its own ad-hoc conventions.
pipelined gives these common programming situations names. By representing these states as data structures rather than keywords or language constructs, we make them explicit, composable, and visible in our types.
A value that may not be present
Section titled “A value that may not be present”We spend a significant amount of our day querying databases, searching maps, and checking configurations for values that might not be there. In standard TypeScript, we represent this with T | null or T | undefined.
This is easy to write, but it forces the caller of a function to immediately stop and write conditional logic. If they forget, strict mode may warn them, but the checking logic itself remains intertwined with the happy path of the program.
Maybe<A> is a data structure representing this choice: it is either Some<A> (a value is present) or None (it is not). Transformations on a Maybe automatically bypass the absent case, allowing us to describe our logic linearly:
If any step produces a None, the subsequent steps are skipped and getOrElse provides the fallback. The presence check is decoupled from the business logic.
An operation that can fail
Section titled “An operation that can fail”Exceptions in JavaScript are wild. They can be thrown by any statement, and their type is always unknown. Nothing in a function’s signature indicates to the caller that it might throw, leading to unhandled runtime failures or defensive try/catch blocks cluttering our code.
Result<E, A> models a synchronous operation that can fail. It is either Ok<A> (representing success) or Err<E> (a typed failure). By returning a Result, we bring the error into the type signature itself, forcing the compiler to verify that the failure is handled.
Collecting multiple failures
Section titled “Collecting multiple failures”When we validate a complex data structure—like a registration form—we often want to present the user with a list of all errors.
If we use a standard Result or try/catch pattern, the program short-circuits at the very first failure. This is correct for sequential business logic, but frustrating for user interfaces where displaying only one error at a time forces a slow, trial-and-error cycle of form submission.
Validation<E, A> is a data structure designed specifically to accumulate errors. It is either Passed<A> or Failed<E>. When we combine multiple validation values, their failures accumulate instead of short-circuiting:
If both fields are invalid, both validation errors are gathered and returned together, enabling clean, comprehensive user feedback.
Asynchronous operations with typed errors
Section titled “Asynchronous operations with typed errors”An asynchronous operation in standard JavaScript is represented by a Promise. Promises are eager—they begin executing the moment they are created—and they can reject with any value.
TaskResult<E, A> is a lazy, infallible blueprint for an asynchronous operation that eventually yields a Result<E, A>. Because it is lazy, nothing runs until we explicitly invoke it. Because it is infallible at the promise level, the outer Task always resolves successfully, carrying our typed Result inside.
The four states of a data fetch
Section titled “The four states of a data fetch”Consider how we typically model a data-fetching operation in UI state:
This model is fragile. It permits invalid combinations: isLoading could be true while data is present, or both error and data could be null simultaneously. Furthermore, the state of “we haven’t requested the data yet” is indistinguishable from “we requested it and found nothing.”
RemoteData<E, A> represents these states explicitly. It is a union of exactly four constructors: NotAsked, Loading, Failure<E>, and Success<A>. The compiler enforces that every state is accounted for:
The journey ahead
Section titled “The journey ahead”These structures are not abstract mathematical exercises. They are simple, practical designs that bring clarity to standard programming patterns.
To understand how to sequence these transformations linearly, read Thinking in pipelines, which covers the mechanics of pipe, flow, and the data-last design that enables them. If you already have a specific problem to solve, you can head directly to the in-depth guides for each module.