Composition means building a bigger function by chaining small ones, so the output of one becomes the input of the next.
const trim = s => s.trim();
const lower = s => s.toLowerCase();
const dashes = s => s.replaceAll(" ", "-");
// Without composition: nested calls read inside out
console.log(dashes(lower(trim(" Hello Big World ")))); // → hello-big-world
pipe: left to right
A tiny helper makes the order read naturally.
const pipe = (...fns) => input => fns.reduce((value, fn) => fn(value), input);
const slugify = pipe(
s => s.trim(),
s => s.toLowerCase(),
s => s.replaceAll(" ", "-")
);
console.log(slugify(" Hello Big World ")); // → hello-big-world
console.log(slugify("JavaScript Is Fun")); // → javascript-is-fun
pipe(a, b, c)(x) means c(b(a(x))), but reads in the order things happen. Its mirror, compose, runs right to left (reduceRight).
Small, single-purpose functions
Composition works best when each function does one thing, and is pure.
const pipe = (...fns) => input => fns.reduce((value, fn) => fn(value), input);
const orders = [
{ id: 1, total: 20, paid: true },
{ id: 2, total: 50, paid: false },
{ id: 3, total: 35, paid: true }
];
const paidOnly = list => list.filter(order => order.paid);
const totals = list => list.map(order => order.total);
const sum = list => list.reduce((a, b) => a + b, 0);
const paidRevenue = pipe(paidOnly, totals, sum);
console.log(paidRevenue(orders)); // → 55
Partial application
Fix some arguments of a function in advance and get a new, smaller function. This is how you adapt functions to fit a pipeline.
const multiply = (a, b) => a * b;
const double = multiply.bind(null, 2);
console.log(double(7)); // → 14
const partial = (fn, ...preset) => (...rest) => fn(...preset, ...rest);
const triple = partial(multiply, 3);
console.log(triple(5)); // → 15
Currying
A curried function takes its arguments one at a time, returning a new function each time.
const add = a => b => a + b;
console.log(add(2)(3)); // → 5
const addTen = add(10);
console.log(addTen(5)); // → 15
const greet = greeting => name => `${greeting}, ${name}!`;
console.log(["Maya", "Sam"].map(greet("Hello"))); // → ["Hello, Maya!", "Hello, Sam!"]
The last line shows why currying is useful: greet("Hello") produces the one-argument function that map needs.
Method chaining is composition too
const result = [5, 3, 8, 1]
.filter(n => n > 2)
.map(n => n * 2)
.sort((a, b) => a - b);
console.log(result); // → [6, 10, 16]
Array methods already form a pipeline: each step returns an array for the next.
Do not overdo it
Composition and currying are tools. A short pipeline of well-named functions is lovely. A chain of nested higher-order helpers that nobody can read is not. If a plain function with a few lines is clearer, write that.