Skip to main content
pipe and flow let you chain multiple functions together without nested calls or intermediate variables. Both are fully type-safe: each function’s return type determines the next function’s input type, and TypeScript errors early if the types don’t connect.
import { pipe, flow } from 'ts-chas/pipe';

pipe — execute immediately

pipe(initialValue, fn1, fn2, fn3) passes initialValue through each function left-to-right and returns the final result:
const add5   = (x: number) => x + 5;
const double = (x: number) => x * 2;
const toStr  = (x: number) => `Result: ${x}`;

const result = pipe(10, add5, double, toStr);
// "Result: 30"
// Equivalent to: toStr(double(add5(10)))
Without pipe, the same code nests outward from the inside:
// Harder to read — you have to read inside-out
const result = toStr(double(add5(10)));

flow — create a reusable function

flow(fn1, fn2, fn3) returns a new function that accepts the first function’s input and returns the last function’s output. Nothing runs until you call the result:
const transform = flow(add5, double, toStr);

transform(10); // "Result: 30"
transform(5);  // "Result: 20"
transform(0);  // "Result: 10"
Use pipe when you have the value now. Use flow when you want to define the transformation once and apply it to multiple values later — for example, as a map callback or a reusable utility.

Type safety

TypeScript checks each step in the pipeline. If the output type of one function doesn’t match the input type of the next, you get a compile-time error:
const broken = pipe(
  'hello',
  (s: string) => s.length, // returns number
  (s: string) => s.trim()  // Error: Argument of type 'number' is not assignable to type 'string'
);

The built-in .pipe() on Result

Result<T, E> and ResultAsync<T, E> have a .pipe() method that threads the full Result through a sequence of functions. Unlike the standalone pipe, these functions receive the Result itself, not just the inner value:
import { chas } from 'ts-chas';

const normalize = (r: chas.Result<string, string>) =>
  r.map((s) => s.trim().toLowerCase());

const validate = (r: chas.Result<string, string>) =>
  r.andThen((s) =>
    s.length > 0 ? chas.ok(s) : chas.err('Empty string')
  );

const result = chas.ok('  Hello World  ').pipe(normalize, validate);
// Ok('hello world')
This is useful for applying reusable Result transformations as a composable pipeline.
The .pipe() method on Result short-circuits on Err ; downstream functions still receive the Result and can choose to handle the error. It does not automatically skip errored steps the way .map() does. Build your pipeline functions to check .isOk() or use .map(), .andThen(), etc. internally.

Practical example

A data transformation pipeline that parses, validates, and formats an API payload:
import { pipe, flow } from 'ts-chas/pipe';
import { chas } from 'ts-chas';
import { is } from 'ts-chas/guard';

// Individual steps — small and testable
const parseId  = (raw: unknown) => is.string.uuid('v4').parse(raw);
const fetchUser = (id: string) => chas.fromPromise(
  fetch(`/api/users/${id}`).then((r) => r.json()),
  () => 'Fetch failed'
);
const formatName = (user: User) =>
  `${user.firstName} ${user.lastName}`.trim();

// Compose into a pipeline
const getUserName = flow(
  (rawId: unknown) => parseId(rawId),
  (result) => result.asyncAndThen(fetchUser),
  (task) => task.map(formatName)
);

const name = await getUserName(requestBody.userId);
// ResultAsync<string, GuardErr | string>