Designing with states
Standard TypeScript models runtime conditions with untyped exceptions (throw), missing pointers (null/undefined), and raw promises (Promise). In multi-step pipelines, these primitives force defensive branching and obscure edge cases in type signatures.
@nlozgachev/pipelined/core represents these runtime states as explicit discriminated unions.
1. Synchronous Data Containers
Section titled “1. Synchronous Data Containers”Absent Values: Maybe<A>
Section titled “Absent Values: Maybe<A>”Accessing missing properties, dictionary keys, or query results yields undefined or null. Handling these with manual if guards interrupts linear pipeline composition.
Maybe<A> represents optionality as a discriminated union: Some<A> (value present) or None (empty). Subsequent mapping steps run only when a value exists:
Typed Synchronous Failures: Result<E, A>
Section titled “Typed Synchronous Failures: Result<E, A>”Functions can throw exceptions of type unknown at any statement without indicating the failure in their return type. Callers must either wrap calls in try/catch or risk unhandled runtime crashes.
Result<E, A> brings failure into the return type: Ok<A> for success or Err<E> for typed errors.
Accumulating Multiple Errors: Validation<E, A>
Section titled “Accumulating Multiple Errors: Validation<E, A>”Result short-circuits on the first failure. While correct for sequential steps, this is counterproductive for form validation and schema parsing where users need a list of all invalid fields at once.
Validation<E, A> evaluates independent checks and accumulates all failures into a non-empty array (NonEmptyArr<E>):
2. Asynchronous Execution & UI States
Section titled “2. Asynchronous Execution & UI States”Lazy Infallible Blueprints: Task.Result<E, A>
Section titled “Lazy Infallible Blueprints: Task.Result<E, A>”Standard Promise<T> begins execution immediately upon instantiation and rejects with untyped errors.
Task.Result<E, A> is a zero-argument function that executes only when called and resolves to a Result<E, A>, guaranteeing the outer promise never rejects:
Explicit UI Lifecycle States: RemoteData<E, A>
Section titled “Explicit UI Lifecycle States: RemoteData<E, A>”Modeling async request state with multiple boolean flags (isLoading, error, data) permits invalid combinations, such as displaying a stale error message while a new request is loading.
RemoteData<E, A> defines the four mutually exclusive states of an asynchronous operation:
3. Concurrency Strategies & Event Pipelines
Section titled “3. Concurrency Strategies & Event Pipelines”Declarative Async Concurrency: Op<I, E, A>
Section titled “Declarative Async Concurrency: Op<I, E, A>”Managing in-flight request cancellation, debouncing, and retries in UI components typically requires manual AbortController handles and state flags.
Op<I, E, A> separates the async task definition from its execution policy:
Typed Event Sequences: Stream<S>
Section titled “Typed Event Sequences: Stream<S>”Standard event emitters leave message payloads untyped, while complex reactive stream frameworks introduce heavy operators for basic event coordination.
Stream<S> provides a type-safe event pipeline for matching event sequences and accumulating state over time:
State Selection Decision Matrix
Section titled “State Selection Decision Matrix”Use this matrix to pick the right data structure for your scenario:
| Situation | Recommended Type | Success State | Failure / Empty State | Execution / Nature |
|---|---|---|---|---|
| Optional value, cache miss, or nullable property | Maybe<A> |
Some<A> |
None |
Synchronous, short-circuits |
| Fallible synchronous business logic | Result<E, A> |
Ok<A> |
Err<E> |
Synchronous, short-circuits |
| Multi-field validation (forms, configs) | Validation<E, A> |
Passed<A> |
Failed<NonEmptyArr<E>> |
Synchronous, accumulates |
| Lazy async operation with typed error channel | Task.Result<E, A> |
Ok<A> |
Err<E> |
Asynchronous, deferred |
| UI data request lifecycle tracking | RemoteData<E, A> |
Success<A> |
Failure<E>, Loading, NotAsked |
State union for UI render |
| Async task needing cancellation, concurrency, or retries | Op<I, E, A> |
Ok<A> |
Err<E> |
Interpreted execution engine |
| Typed pub/sub event pipeline & state accumulation | Stream<S> |
Message<S> |
N/A | Reactive event bus |
Universal Operations Across Structures
Section titled “Universal Operations Across Structures”Every data container in pipelined shares a consistent, predictable set of combinator names. Once you learn how an operation behaves on Maybe, you already know how it works across Result, Validation, Task.Result, and RemoteData:
| Operation | Purpose | Example |
|---|---|---|
map |
Transforms the inner success value while preserving the container structure. | Result.map((n) => n * 2) |
mapError |
Transforms the error payload without modifying success values. | Result.mapError((e) => e.message) |
chain |
Sequences a function returning another container, flattening nested layers. | Maybe.chain((user) => lookup(user.id)) |
getOrElse |
Unwraps the value, falling back to a default thunk if absent or failed. | Maybe.getOrElse(() => DEFAULT_USER) |
match |
Exhaustively unwraps all variants using a named-case object. | RemoteData.match({ loading, success, failure, notAsked }) |
fold |
Exhaustively unwraps all variants using positional callbacks. | Result.fold(onErr, onOk) |
tap |
Intercepts the success value for side effects without modifying the data. | Task.Result.tap((u) => log(u.id)) |
recover |
Catches a failure variant and substitutes a fallback container. | Result.recover((err) => Result.make.ok(cached)) |
Standardized Drawers
Section titled “Standardized Drawers”To keep root module objects clean and focused on pipeline combinators, instantiation, conversions, and type guards are organized into four standard drawers across every module:
| Drawer | Purpose | Examples |
|---|---|---|
make.* |
Instantiates a specific variant of a discriminated union. | Result.make.ok(42)Maybe.make.some(5)RemoteData.make.loading() |
from.* |
Converts external types or primitives into the container. | Maybe.from.nullable(val)Result.from.Validation(fn)Tuple.from.pair(a, b) |
to.* |
Converts the container back to language primitives or other containers. | Maybe.to.nullable(maybe)Result.to.Maybe(result)Str.to.number(str) |
is.* |
Type guard predicates for conditional branching and filtering. | Result.is.ok(result)Arr.is.empty(array)Num.is.even(n) |
End-to-End Workflow
Section titled “End-to-End Workflow”In a real-world feature, these modules compose across boundaries: validating user input, executing a cancellable network call, and updating UI state: