Currying in JavaScript
What is Currying?
Imagine you have a function that needs three numbers to work: add(a, b, c).
Normally, you’d call it like add(2, 3, 5) and get the answer right away.
Currying lets you break this into smaller steps:
OK, but why?
Currying is particularly useful when you want to create specialized functions from more general ones. Consider an API client where you need to make requests to different endpoints with consistent configuration. Before, without currying:
const request = async (baseUrl, method, endpoint, body) => {
const response = await fetch(`${baseUrl}${endpoint}`, {
method,
body: body ? JSON.stringify(body) : undefined,
});
return response.json();
};
// Every call requires repeating params
await request("https://api.example.com", "GET", "/users", null);
await request("https://api.example.com", "POST", "/users", { name: "Alice" });
await request("https://api.example.com", "GET", "/posts", null);
After, with currying:
const _request = async (baseUrl, method, endpoint, body) => {
const response = await fetch(`${baseUrl}${endpoint}`, {
method,
body: body ? JSON.stringify(body) : undefined,
});
return response.json();
};
const request = curry(_request);
// Configure once, use everywhere
const api = request("https://api.example.com");
const get = api("GET");
const post = api("POST");
await get("/users");
await post("/users", { name: "Alice" });
await get("/posts");
Implementation
So, how do we implement the curry function from the previous example?
The key idea is to check if we have received enough arguments yet. Every
function in JavaScript has a .length property that tells us how many
parameters it expects. When someone calls our curried function, we compare the
number of arguments they passed against this expected count.
If we have all the arguments we need, we simply call the original function with them. But if we’re still missing some, we return a new function that captures the arguments we’ve received so far in a closure. When this new function is called with more arguments, it combines them with the ones it captured in the closure and checks again. This process repeats until we’ve accumulated enough arguments to finally execute the original function.
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
Dry Run: Tracing add(1)(2)(3)
Let’s trace through exactly what happens when we call our curried add function: