Technology · JavaScript

Advanced

JavaScript Currying and Composition

Transform multi-argument functions into chained calls and combine small functions into pipelines.

TL;DR
  1. 01Curry transforms a multi-arg function into nested single-arg calls.
  2. 02Partial application fixes some arguments and returns a new function.
  3. 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 curry function 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.length reports 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.bind is the built-in way to partially apply arguments in JavaScript.
  • Write a small partial helper when you need it without relying on bind.
    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

  • compose combines 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!"
    
  • pipe runs 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 reduce internally 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
  1. 01Write small, single-purpose functions first, since currying and composition only pay off when each piece does one clear thing.
  2. 02Use pipe for left-to-right pipelines that mirror reading order, and reserve compose for math-style right-to-left function notation.
Warnings
  1. 01Currying every function by default adds indirection and hurts readability when it's only called once with all its arguments.
  2. 02Composed pipelines swallow intermediate errors silently unless each function validates its own input before passing data forward.
  3. 03Arrow functions returned from a generic curry helper lose access to arguments.length, so variadic functions need explicit arity instead.
FAQ
JavaScript ClassesJavaScript Event Loop