How to Use Currying in JavaScript
Currying is one of those concepts that sounds intimidating at first but, once understood, becomes a natural tool in your JavaScript toolkit. It transforms how you think about functions, letting you build small, reusable pieces that snap together like building blocks.
Rooted in functional programming and named after mathematician Haskell Curry, currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument. Instead of calling f(a, b, c), you call f(a)(b)(c).
This guide explains what currying is, how to implement it from scratch, how it differs from partial application, and where it shines in real-world JavaScript code.
What Is Currying?
Currying transforms a function with multiple parameters into a chain of functions, each accepting one argument at a time.
A regular function that takes three arguments:
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
The curried version of the same function:
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6
Each call returns a new function that "remembers" the previously passed argument through closures. The computation only happens when all arguments have been provided.
With arrow functions, the curried version becomes much more concise:
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3); // 6
Step-by-Step Breakdown
Let's trace exactly what happens with curriedAdd(1)(2)(3):
const curriedAdd = a => b => c => a + b + c;
// Step 1: curriedAdd(1) returns a function that remembers a = 1
const step1 = curriedAdd(1); // b => c => 1 + b + c
// Step 2: step1(2) returns a function that remembers a = 1, b = 2
const step2 = step1(2); // c => 1 + 2 + c
// Step 3: step2(3) finally computes the result
const result = step2(3); // 1 + 2 + 3 = 6
console.log(result); // 6
The power here is not in the final call. It is in the intermediate functions. Each step produces a specialized, reusable function:
const addTen = curriedAdd(10); // b => c => 10 + b + c
const addTenAndFive = addTen(5); // c => 10 + 5 + c
console.log(addTenAndFive(1)); // 16
console.log(addTenAndFive(20)); // 35
console.log(addTen(0)(0)); // 10
Implementing Curry: Simple and Advanced
Manually currying every function by hand is tedious. What we really want is a curry helper that transforms any regular function into a curried one automatically.
Simple Implementation (Fixed Arity)
The simplest curry function works with functions that have a known number of parameters:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
Let's break down what this does:
- It checks if enough arguments have been collected (
args.length >= fn.length) - If yes, it calls the original function with all collected arguments
- If no, it returns a new function that waits for more arguments and concatenates them
function multiply(a, b, c) {
return a * b * c;
}
const curriedMultiply = curry(multiply);
// All of these produce the same result:
console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
console.log(curriedMultiply(2)(3, 4)); // 24
console.log(curriedMultiply(2, 3, 4)); // 24
Notice that this implementation is flexible: you can pass one argument at a time, or pass several at once. This makes it much more practical than strict one-argument-at-a-time currying.
How fn.length Works
The implementation relies on fn.length, which returns the number of parameters a function declares:
function one(a) {}
function two(a, b) {}
function three(a, b, c) {}
console.log(one.length); // 1
console.log(two.length); // 2
console.log(three.length); // 3
fn.length does not count rest parameters or parameters with default values:
function example(a, b = 10, ...rest) {}
console.log(example.length); // 1 (only counts `a`)
This means the simple curry implementation will not work correctly with functions that use default parameters or rest parameters.
Advanced Implementation (Preserving Context)
The simple version works for most cases. Here is a more robust version that handles edge cases:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// Create a bound function that preserves `this`
const partial = (...nextArgs) => {
return curried.apply(this, [...args, ...nextArgs]);
};
// Preserve the remaining arity for inspection
Object.defineProperty(partial, 'length', {
value: fn.length - args.length
});
return partial;
};
}
Testing with object methods:
const calculator = {
multiplier: 10,
compute: curry(function(a, b) {
return (a + b) * this.multiplier;
})
};
console.log(calculator.compute(2)(3)); // 50 - i.e. (2 + 3) * 10
An Even Simpler Approach for Two Arguments
If you frequently curry functions with exactly two parameters, a dedicated helper is clean and efficient:
const curry2 = fn => a => b => fn(a, b);
const pow = curry2(Math.pow);
const square = pow(2);
// Note: Math.pow(base, exponent), so pow(2) creates base=2
// This means square(n) computes 2^n, not n^2
console.log(square(3)); // 8 (2^3)
console.log(square(10)); // 1024 (2^10)
Partial Application vs. Currying
Currying and partial application are closely related but not the same thing. They are often confused, so let's clarify the difference.
Currying
Currying transforms a function of N arguments into N nested functions of one argument each. The transformation is about the structure of the function:
// Original: f(a, b, c)
// Curried: f(a)(b)(c)
const curriedSum = a => b => c => a + b + c;
curriedSum(1)(2)(3); // 6
Partial Application
Partial application fixes some arguments of a function and returns a new function that takes the remaining arguments. The result is a function with fewer parameters:
function sum(a, b, c) {
return a + b + c;
}
// Partially apply the first argument
function partialSum(b, c) {
return sum(10, b, c); // `a` is fixed to 10
}
partialSum(2, 3); // 15
Using Function.prototype.bind() for partial application:
function sum(a, b, c) {
return a + b + c;
}
const addTen = sum.bind(null, 10); // fixes a = 10
console.log(addTen(2, 3)); // 15
const addTenAndFive = sum.bind(null, 10, 5); // fixes a = 10, b = 5
console.log(addTenAndFive(3)); // 18
Side-by-Side Comparison
function volume(length, width, height) {
return length * width * height;
}
// --- Currying ---
const curriedVolume = curry(volume);
curriedVolume(2)(3)(4); // 24
curriedVolume(2)(3); // returns a function waiting for height
curriedVolume(2); // returns a function waiting for width and height
// --- Partial Application ---
const boxVolume = volume.bind(null, 2); // length fixed to 2
boxVolume(3, 4); // 24
const flatBoxVolume = volume.bind(null, 2, 3); // length=2, width=3 fixed
flatBoxVolume(4); // 24
| Feature | Currying | Partial Application |
|---|---|---|
| What it does | Transforms function structure | Fixes specific arguments |
| Arguments per call | One at a time (strict) or flexible | Remaining unfixed arguments |
| Returns | Chain of single-argument functions | One function with fewer parameters |
| Reusability | Every intermediate step is reusable | Creates one specialized function |
In practice, the curry helper we built earlier blends both concepts. It supports currying (one argument at a time) and partial application (multiple arguments at once). This is intentional and matches how libraries like Lodash implement it.
A Generic Partial Application Helper
While bind works for partial application, you can build a dedicated helper:
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function greet(greeting, punctuation, name) {
return `${greeting}, ${name}${punctuation}`;
}
const greetHello = partial(greet, "Hello", "!");
console.log(greetHello("Alice")); // "Hello, Alice!"
console.log(greetHello("Bob")); // "Hello, Bob!"
const greetHi = partial(greet, "Hi", ".");
console.log(greetHi("Charlie")); // "Hi, Charlie."
Practical Uses
Currying shines in several real-world scenarios. Let's explore the most common ones.
Reusable Utility Functions
Create specialized versions of general functions:
const curry = fn => function curried(...args) {
return args.length >= fn.length
? fn(...args)
: (...next) => curried(...args, ...next);
};
// A general-purpose filter
const filter = curry((predicate, array) => array.filter(predicate));
// Create specialized filters
const getEvenNumbers = filter(n => n % 2 === 0);
const getPositive = filter(n => n > 0);
const getLongStrings = filter(s => s.length > 5);
console.log(getEvenNumbers([1, 2, 3, 4, 5, 6])); // [2, 4, 6]
console.log(getPositive([-2, -1, 0, 1, 2])); // [1, 2]
console.log(getLongStrings(["hi", "hello", "wonderful"])); // ["wonderful"]
Composing Data Transformations
Curried functions are perfect for building data pipelines:
const map = curry((fn, array) => array.map(fn));
const filter = curry((predicate, array) => array.filter(predicate));
const reduce = curry((fn, initial, array) => array.reduce(fn, initial));
const prop = curry((key, obj) => obj[key]);
const pipe = (...fns) => input => fns.reduce((acc, fn) => fn(acc), input);
const users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 30, active: false },
{ name: "Charlie", age: 35, active: true },
{ name: "Diana", age: 28, active: true }
];
// Build a pipeline: get names of active users over 26
const getActiveAdultNames = pipe(
filter(user => user.active),
filter(user => user.age > 26),
map(prop("name"))
);
console.log(getActiveAdultNames(users)); // ["Charlie", "Diana"]
Each piece of this pipeline is a small, testable, reusable function. Currying is what makes filter(user => user.active) and map(prop("name")) possible as standalone expressions.
Configuration and Factory Functions
Currying naturally creates configurable functions where you set up options first and data later:
const createLogger = curry((level, prefix, message) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] [${prefix}] ${message}`);
});
// Create specialized loggers
const errorLog = createLogger("ERROR");
const appError = errorLog("APP");
const dbError = errorLog("DATABASE");
const infoLog = createLogger("INFO");
const appInfo = infoLog("APP");
appError("Connection refused");
// [2024-01-15T10:30:00.000Z] [ERROR] [APP] Connection refused
dbError("Query timeout");
// [2024-01-15T10:30:00.000Z] [ERROR] [DATABASE] Query timeout
appInfo("Server started");
// [2024-01-15T10:30:00.000Z] [INFO] [APP] Server started
API Request Builders
const request = curry((baseURL, method, endpoint, data) => {
console.log(`${method} ${baseURL}${endpoint}`, data || "");
return fetch(`${baseURL}${endpoint}`, {
method,
headers: { "Content-Type": "application/json" },
body: data ? JSON.stringify(data) : undefined
});
});
// Configure for a specific API
const api = request("https://api.example.com");
// Create method-specific functions
const apiGet = api("GET");
const apiPost = api("POST");
const apiPut = api("PUT");
const apiDelete = api("DELETE");
// Use them
apiGet("/users");
apiPost("/users", { name: "Alice", email: "alice@example.com" });
apiPut("/users/1", { name: "Alice Updated" });
apiDelete("/users/1", null);
Event Handler Factories
const handleEvent = curry((action, elementId, event) => {
console.log(`Action: ${action}, Element: ${elementId}, Type: ${event.type}`);
});
const handleClick = handleEvent("click");
const handleSubmit = handleEvent("submit");
// Each returns a function that accepts the event object
document.getElementById("btn-save").addEventListener("click", handleClick("btn-save"));
document.getElementById("btn-delete").addEventListener("click", handleClick("btn-delete"));
document.getElementById("myForm").addEventListener("submit", handleSubmit("myForm"));
Validation Chains
const validate = curry((ruleName, ruleConfig, value) => {
switch (ruleName) {
case "minLength":
return value.length >= ruleConfig
? { valid: true }
: { valid: false, error: `Must be at least ${ruleConfig} characters` };
case "maxLength":
return value.length <= ruleConfig
? { valid: true }
: { valid: false, error: `Must be at most ${ruleConfig} characters` };
case "matches":
return ruleConfig.test(value)
? { valid: true }
: { valid: false, error: `Does not match required pattern` };
default:
return { valid: true };
}
});
// Create reusable validators
const minLength = validate("minLength");
const maxLength = validate("maxLength");
const matches = validate("matches");
const min3 = minLength(3);
const max50 = maxLength(50);
const isEmail = matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
console.log(min3("hi")); // { valid: false, error: "Must be at least 3 characters" }
console.log(min3("hello")); // { valid: true }
console.log(isEmail("test@a.com")); // { valid: true }
console.log(isEmail("not-email")); // { valid: false, error: "Does not match required pattern" }
String Formatting
const formatWith = curry((template, ...values) => {
return values.reduce((str, val, i) => str.replace(`{${i}}`, val), template);
});
const greetTemplate = formatWith("Hello, {0}! Welcome to {1}.");
console.log(greetTemplate("Alice", "JavaScript"));
// "Hello, Alice! Welcome to JavaScript."
const errorTemplate = formatWith("Error {0}: {1} at line {2}");
console.log(errorTemplate("404", "Not Found", "42"));
// "Error 404: Not Found at line 42"
Lodash _.curry
The Lodash library provides a battle-tested _.curry function that handles edge cases you might not think of. It is the most widely used curry implementation in the JavaScript ecosystem.
Basic Usage
import _ from "lodash";
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = _.curry(add);
// All calling styles work:
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
curriedAdd(1, 2, 3); // 6
Placeholder Arguments with _.curry.placeholder
Lodash supports placeholders, allowing you to skip arguments and fill them in later. This enables currying arguments in any order, not just left to right:
import _ from "lodash";
function formatDate(year, month, day) {
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
const curriedFormat = _.curry(formatDate);
// Use placeholder (_) to skip the year
const formatThisYear = curriedFormat(2024);
console.log(formatThisYear(1, 15)); // "2024-01-15"
console.log(formatThisYear(12, 25)); // "2024-12-25"
// Skip the first argument, fix month to December
const decemberOf = curriedFormat(_, 12);
console.log(decemberOf(2024)(25)); // "2024-12-25"
console.log(decemberOf(2023)(31)); // "2023-12-31"
_.curryRight
Lodash also provides _.curryRight, which applies arguments from right to left:
import _ from "lodash";
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const curriedGreet = _.curryRight(greet);
const greetAlice = curriedGreet("Alice");
console.log(greetAlice("Hello")); // "Hello, Alice!"
console.log(greetAlice("Hi")); // "Hi, Alice!"
Custom Arity
You can specify the arity (number of expected arguments) explicitly, which is useful for functions with default or rest parameters:
import _ from "lodash";
function sum(a, b, c = 0) {
return a + b + c;
}
console.log(sum.length); // 2 (default params not counted)
// Force curry to expect 3 arguments
const curriedSum = _.curry(sum, 3);
console.log(curriedSum(1)(2)(3)); // 6
Implementing a Lodash-Like Placeholder System
If you do not want to import Lodash, here is a simplified implementation with placeholder support:
const _ = Symbol("placeholder");
function curryWithPlaceholder(fn) {
const arity = fn.length;
return function curried(...args) {
// Check if we have enough non-placeholder args
const realArgs = args.filter(a => a !== _);
if (realArgs.length >= arity) {
return fn(...args.filter(a => a !== _));
}
return function(...nextArgs) {
// Replace placeholders with next arguments
const merged = [];
let nextIndex = 0;
for (const arg of args) {
if (arg === _ && nextIndex < nextArgs.length) {
merged.push(nextArgs[nextIndex++]);
} else {
merged.push(arg);
}
}
// Append any remaining nextArgs
while (nextIndex < nextArgs.length) {
merged.push(nextArgs[nextIndex++]);
}
return curried(...merged);
};
};
}
function volume(l, w, h) {
return l * w * h;
}
const curriedVolume = curryWithPlaceholder(volume);
const withHeight10 = curriedVolume(_, _, 10);
console.log(withHeight10(2, 3)); // 60 (2 * 3 * 10)
console.log(withHeight10(5, 5)); // 250 (5 * 5 * 10)