Technology · JavaScript
AdvancedJavaScript Currying and Composition
Transform multi-argument functions into chained calls and combine small functions into pipelines.
TL;DR
- 01Curry transforms a multi-arg function into nested single-arg calls.
- 02Partial application fixes some arguments and returns a new function.
- 03Compose and pipe combine functions into a single data pipeline.
What Currying Does
- Currying converts a function that takes many arguments into a chain of functions that each take one.
function add(a, b, c) { return a + b + c; } function curriedAdd(a) { return (b) => (c) => a + b + c; } curriedAdd(1)(2)(3); // 6 - Each call in the chain returns a new function until all arguments have been supplied.
- The original function only runs once every argument has arrived.
- Curried functions make it easy to fix one argument and reuse the rest later.
const addFive = curriedAdd(5); addFive(2)(3); // 10 - Currying works best on pure functions with a fixed, known number of arguments.
- Arrow function chains are the most common way to write curried functions by hand.
A Generic Curry Helper
- Build a reusable
curryfunction so you don't hand-write nested arrows for every function.function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } return (...next) => curried.apply(this, [...args, ...next]); }; } fn.lengthreports how many named parameters the original function expects.const sum3 = curry((a, b, c) => a + b + c); sum3(1)(2)(3); // 6 sum3(1, 2)(3); // 6 sum3(1, 2, 3); // 6- A curried function accepts arguments one at a time or in groups, since it just keeps collecting until it has enough.
- Rest and default parameters don't count toward
fn.length, so curry generic functions explicitly instead.function manualCurry(fn, arity) { return function collect(...args) { return args.length >= arity ? fn(...args) : (...more) => collect(...args, ...more); }; } - Libraries like lodash and Ramda ship battle-tested curry implementations worth using in production.
Partial Application
- Partial application fixes some arguments now and returns a function waiting for the rest.
function multiply(a, b) { return a * b; } const double = multiply.bind(null, 2); double(5); // 10 Function.prototype.bindis the built-in way to partially apply arguments in JavaScript.- Write a small
partialhelper when you need it without relying onbind.function partial(fn, ...fixed) { return (...rest) => fn(...fixed, ...rest); } const greetHello = partial((greeting, name) => `${greeting}, ${name}!`, 'Hello'); greetHello('Ada'); // "Hello, Ada!" - Partial application is useful for creating preset configurations, like a logger fixed to one log level.
- Unlike currying, a partially applied function can accept more than one argument per call.
- Use partial application when you only need to fix arguments once, not build a full chain.
Composing Functions
composecombines functions so the rightmost one runs first, then each result feeds the next function leftward.const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x); const shout = (s) => s.toUpperCase() + '!'; const exclaim = compose(shout, (s) => s.trim()); exclaim(' hi '); // "HI!"piperuns the same functions left-to-right, which often matches how you read the steps.const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x); const process = pipe((s) => s.trim(), (s) => s.toUpperCase(), (s) => s + '!'); process(' hi '); // "HI!"- Every function in a pipeline should take one input and return one output of a compatible type.
- Compose and pipe both use
reduceinternally to thread a single value through each function. - Keep composed functions pure so the pipeline stays predictable and easy to test in isolation.
- Name each step clearly, since a long pipeline of anonymous arrows is hard to debug later.
Practical Use Cases
- Build reusable validators by currying a rule function with its threshold first.
const minLength = curry((min, str) => str.length >= min); const isValidUsername = minLength(3); isValidUsername('ab'); // false isValidUsername('abc'); // true - Chain middleware-style transformations with pipe to process data in clear, ordered steps.
const processOrder = pipe( applyDiscount, addTax, formatCurrency ); processOrder(100); - Curry event handlers to bind context-specific data without writing a new closure each time.
const handleClick = curry((id, event) => console.log('clicked', id, event.type)); button.addEventListener('click', handleClick(42)); - Compose array transformations to avoid intermediate variable names for each step.
const summary = pipe( (items) => items.filter((i) => i.active), (items) => items.map((i) => i.price), (prices) => prices.reduce((a, b) => a + b, 0) ); - Use currying to adapt functions to APIs that expect a single-argument callback, like array methods.
Tips
- 01Write small, single-purpose functions first, since currying and composition only pay off when each piece does one clear thing.
- 02Use pipe for left-to-right pipelines that mirror reading order, and reserve compose for math-style right-to-left function notation.
Warnings
- 01Currying every function by default adds indirection and hurts readability when it's only called once with all its arguments.
- 02Composed pipelines swallow intermediate errors silently unless each function validates its own input before passing data forward.
- 03Arrow functions returned from a generic curry helper lose access to arguments.length, so variadic functions need explicit arity instead.
FAQ
- Currying transforms a function so it takes one argument at a time, returning a new function until all arguments arrive. Partial application fixes a subset of arguments up front and returns a function expecting the rest, in one step. Curried functions can be partially applied, but not every partially applied function is curried.
- Check the target function's arity with fn.length, then return a wrapper that collects arguments until it has enough. Once enough arguments are collected, call the original function with them. Most implementations use recursion to keep collecting args across multiple calls.
- Both combine functions into one, but they run in opposite order. Compose runs functions right-to-left, matching mathematical notation like f(g(x)). Pipe runs functions left-to-right, which often reads more naturally as a sequence of steps.
- Currying lets you create specialized functions by fixing some arguments early, like a validator preset with a specific rule. This avoids repeating the same arguments across many calls. It also makes functions easier to plug into pipelines that expect a single input.
- Curried calls create extra closures and function calls compared to a single direct call. For most application code this overhead is negligible. Avoid currying inside tight loops or hot paths where the extra function calls add measurable cost.