Skip to main content

Implementing compose

compose() is a combinator that assembles several functions into one: compose(f, g, h)(x) is equivalent to f(g(h(x))) and runs them right to left. The whole implementation boils down to folding a list of functions: reduceRight for compose and reduce for its mirror image pipe.

Theory

TL;DR

  • compose(f, g, h)(x) equals f(g(h(x))), executing right to left.
  • The core implementation is fns.reduceRight((v, fn) => fn(v), x).
  • To let the last function in the chain take several arguments, keep the accumulator in an array and unpack it with spread.
  • The async variant wraps the first result in Promise.resolve and stitches the rest together with .then.
  • In TypeScript you write overloads for each arity, because a general variadic type is hard to infer.
  • In production it is handy to have both: compose (right to left) and pipe (left to right), plus their async versions.

Quick example

javascript
const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x); const trim = s => s.trim(); const toInt = s => parseInt(s, 10); const inc = n => n + 1; const parseAndInc = compose(inc, toInt, trim); console.log(parseAndInc(' 41 ')); // 42

The string is trimmed first, then parsed into a number, and only then incremented: the functions read right to left.

Minimal implementation (ES6)

A generic version in which the last handler can take many arguments:

javascript
// Generic version: the last handler can take many arguments const compose = (...fns) => (...args) => fns.reduceRight( (acc, fn) => [fn(...acc)], args )[0]; // Example const trim = s => s.trim(); const toInt = s => parseInt(s, 10); const inc = n => n + 1; const parseAndInc = compose(inc, toInt, trim); parseAndInc(' 41 '); // 42

The trick is that the accumulator is always an array of arguments. The seed value is args, and every step returns [fn(...acc)], that is an array of one element again. At the end you just take [0].

A variant for unary functions

If every function takes exactly one argument, the array wrapper is redundant and the implementation becomes shorter and slightly faster:

javascript
const composeUnary = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);

This is the most common form: in functional code chains are almost always made of unary transformations.

Promise-aware compose

The async version works with both sync and async functions:

javascript
const composeAsync = (...fns) => (...args) => fns .slice(0, -1) .reduceRight( (p, fn) => p.then(res => fn(res)), Promise.resolve(fns[fns.length - 1](...args)) ); // Example const fetchUser = async id => ({ id, name: 'Tim' }); const getName = u => u.name; const shout = s => s.toUpperCase(); const getUserNameLoud = composeAsync(shout, getName, fetchUser); getUserNameLoud(7).then(console.log); // 'TIM'

The last function in the list (fetchUser) is called separately and its result is immediately normalised with Promise.resolve. The remaining functions are attached with .then, so each one receives an already unwrapped value, whether it is sync or async.

A type-safe version for TypeScript

A general type for a variadic function is hard to infer, so in practice people write overloads. The ones below cover up to 4 functions and can be extended the same way.

typescript
type Unary<A, R> = (a: A) => R; export function compose<A, R>(f1: Unary<A, R>): Unary<A, R>; export function compose<A, B, R>( f1: Unary<B, R>, f2: Unary<A, B> ): Unary<A, R>; export function compose<A, B, C, R>( f1: Unary<C, R>, f2: Unary<B, C>, f3: Unary<A, B> ): Unary<A, R>; export function compose<A, B, C, D, R>( f1: Unary<D, R>, f2: Unary<C, D>, f3: Unary<B, C>, f4: Unary<A, B> ): Unary<A, R>; export function compose(...fns: Function[]) { return (x: unknown) => fns.reduceRight((v, f) => f(v), x); }

The overloads stitch the types of neighbouring functions together: the output of f2 must match the input of f1, otherwise the compiler flags it right away.

pipe and useful notes

You often need pipe() as well, which runs functions left to right:

javascript
const pipe = (...fns) => (...args) => fns.reduce((acc, fn) => [fn(...acc)], args)[0]; const parseAndInc2 = pipe(trim, toInt, inc); parseAndInc2(' 41 '); // 42
  • If you compose object methods, do not forget this: bind them with fn.bind(obj).
  • Try to keep functions pure and unary: composition then stays simpler and more predictable.
  • In production it is convenient to have both: compose (right to left) and pipe (left to right), plus composeAsync / pipeAsync.

Common mistakes

  • Reaching for reduce instead of reduceRight. That gives you pipe, not compose, and the chain runs in reverse.
  • Forgetting the seed value in reduce. Without it the first accumulator is a function from the list rather than the input data.
  • Expecting a plain compose to unwrap a Promise. The sync version passes the Promise itself along; you need composeAsync.
  • Losing this on methods. compose(obj.method) calls the method without its context; use obj.method.bind(obj).
  • Counting on several arguments in the middle of the chain. Only the last (rightmost) function takes multiple arguments; every later step receives exactly one value.
  • Composing impure functions. Side effects in the middle of a chain make the result depend on call order and timing.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.