Back to stories
<Frontend/>

JavaScript Pipe and Compose: Function Composition Utilities

Share by

JavaScript Pipe and Compose: Function Composition Utilities

Pipe and compose are small utilities that combine multiple functions into one. Pipe runs them left-to-right (first function, then second, then third); compose runs them right-to-left (last function’s output into the previous, and so on). Both give you a clear “pipeline” of transformations without nesting or intermediate variables. This post explains how they work, how to implement them, and when to use each.


What pipe and compose do

You have several single-responsibility functions (e.g. trim, toLowerCase, capitalize) and want to apply them in sequence. Instead of:

const result = capitalize(toLowerCase(trim(input)));

you can write:

const process = pipe(trim, toLowerCase, capitalize);
const result = process(input);
  • pipe(f, g, h)(x) means: run f(x), then g on that result, then h on that. Order: left → right (same as reading).
  • compose(f, g, h)(x) means: run h(x), then g on that, then f. Order: right → left (math composition: (f ∘ g ∘ h)(x)).

So pipe(trim, toLowerCase, capitalize) is the same as compose(capitalize, toLowerCase, trim): same functions, opposite order in the argument list.


Implementing pipe and compose

Pipe — pass the initial value through the first function, then feed each result into the next:

function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
  return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}

Compose — same idea but run the functions in reverse order (reduceRight, or reverse then reduce):

function compose<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
  return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}

For mixed input/output types you can use a generic chain (each function’s output is the next function’s input). A simple typed version:

function pipe<A, B>(f: (a: A) => B): (a: A) => B;
function pipe<A, B, C>(f: (a: A) => B, g: (b: B) => C): (a: A) => C;
function pipe<A, B, C, D>(
  f: (a: A) => B,
  g: (b: B) => C,
  h: (c: C) => D,
): (a: A) => D;
function pipe(
  ...fns: Array<(arg: unknown) => unknown>
): (arg: unknown) => unknown {
  return (arg: unknown) => fns.reduce((acc, fn) => fn(acc), arg);
}

In practice, many codebases use a single generic pipe and accept that TypeScript may infer a wider type; the runtime behavior is the same.


Pipe: left-to-right pipelines

Pipe matches how we read: “first trim, then lowercase, then capitalize.” Good for:

  • String cleaning/normalization
  • Validation chains (each step returns the value or throws)
  • Data transformation pipelines (parse → validate → map → serialize)

Example:

const trim = (s: string) => s.trim();
const toLowerCase = (s: string) => s.toLowerCase();
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
const append = (suffix: string) => (s: string) => s + suffix;

const formatTitle = pipe(trim, toLowerCase, capitalize, append("."));
formatTitle("  hello world  "); // 'Hello world.'

Compose: right-to-left composition

Compose matches mathematical function composition: (f ∘ g)(x) = f(g(x)). The rightmost function runs first. Some developers prefer it when they think “last step applied last” or when building from small pieces:

const formatTitle = compose(append("."), capitalize, toLowerCase, trim);
formatTitle("  hello world  "); // 'Hello world.'

Same result as the pipe above; only the order of arguments to compose is reversed. Use compose when you like “pipeline order in the list = execution order” (top of list = last to run). Use pipe when you want “list order = execution order” (top of list = first to run).


Pipe and compose vs method chaining

  • Method chaining (e.g. arr.filter(...).map(...)) requires the value to have those methods. Each step returns a value that has the next method.
  • Pipe/compose work on any value; they just call plain functions. You’re not limited to array methods or a specific API.

So you use chaining when you have a single type (e.g. array) and a fixed set of methods. You use pipe/compose when you want to combine arbitrary pure functions, including ones that change type (e.g. string → number → boolean).

You can mix both: e.g. a function that returns an array, then pipe that into a chain, or the other way around.


When to use which

  • Prefer pipe when you want “first step first” in the argument list—easier for most people to read.
  • Use compose when you prefer math notation or when you’re building a pipeline by “adding a step at the front” (e.g. compose(newStep, ...existingPipeline)).
  • Use neither when one or two steps are enough; a simple nested call or two variables is often clearer.

Summary

  • pipe(f, g, h) runs f then g then h (left to right); compose(f, g, h) runs h then g then f (right to left).
  • Both are one-liners with reduce / reduceRight and help avoid deep nesting and one-off variables.
  • Prefer pipe for readability when the list order is “first step to last step”; use compose when you like mathematical order or composing by prepending steps.
  • They work with any functions and types, unlike method chaining which is tied to the methods on a value.