Curry a Function in JavaScript
Currying turns a multi-argument function into a chain of single-argument calls so you can pre-bind some arguments and pass the rest later. This snippet covers the variadic curry every codebase reaches for, a placeholder-aware version for skipping argument positions, and a typed-friendly fixed-arity helper for stricter call shapes. Use it for partial application and to build small DSLs.
699 views
3
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn.apply(this, args);
return (...rest) => curried.apply(this, [...args, ...rest]);
};
}
const add3 = curry((a, b, c) => a + b + c);
console.log(add3(1, 2, 3)); // 6
console.log(add3(1)(2)(3)); // 6
console.log(add3(1, 2)(3)); // 6
console.log(add3(1)(2, 3)); // 6The classic implementation reads fn.length (the declared parameter count) and keeps collecting arguments until at least that many have been supplied. As long as fewer arguments arrive, it returns a fresh function that prepends the captured args to the next batch. Using fn.apply(this, args) keeps the original this binding, which matters when the curried target is a method. The base case is one line; the recursive case is one line. Use this version whenever you want partial application that supports both f(1, 2) and f(1)(2) call shapes.
const _ = Symbol('placeholder');
function curryP(fn) {
function curried(...args) {
const filled = args.slice(0, fn.length);
const hasPlaceholder = filled.length < fn.length || filled.includes(_);
if (!hasPlaceholder) return fn.apply(this, filled);
return function next(...rest) {
const merged = [];
let restIdx = 0;
for (let i = 0; i < Math.max(filled.length, fn.length); i++) {
if (filled[i] === _ && restIdx < rest.length) {
merged.push(rest[restIdx++]);
} else if (i < filled.length) {
merged.push(filled[i]);
} else if (restIdx < rest.length) {
merged.push(rest[restIdx++]);
}
}
return curried.apply(this, merged);
};
}
return curried;
}
const greet = curryP((greeting, name, punct) => `${greeting}, ${name}${punct}`);
const sayHiTo = greet('Hi', _, '!');
console.log(sayHiTo('Ada')); // Hi, Ada!
const yellAt = greet(_, 'Lin', '!!!');
console.log(yellAt('Hey')); // Hey, Lin!!!Plain curry forces you to fill arguments left-to-right, but real APIs often want the third argument bound while the first stays variable (the standard Lodash pattern). A Symbol('placeholder') marks a hole, and the merge loop fills holes in order with whatever the next call provides before falling through to remaining positionals. The export pattern (a singleton _) gives every consumer the same identity-equal token. Reach for this version when building point-free pipelines or when a library exposes a curried helper that callers should be able to skip into.
function curryN(arity, fn) {
return function curried(...args) {
if (args.length >= arity) return fn.apply(this, args.slice(0, arity));
return (...rest) => curried.apply(this, [...args, ...rest]);
};
}
// Variadic functions don't expose a useful `length`, so curryN takes the count.
function sumAll(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
const sum4 = curryN(4, sumAll);
console.log(sum4(1)(2)(3)(4)); // 10
console.log(sum4(1, 2)(3, 4)); // 10
console.log(sum4(1)(2, 3, 4, 99)); // 10 (extra args are dropped)fn.length returns 0 for function f(...args) {} and stops counting at the first default-valued or rest parameter, so the variadic curry above silently breaks for those signatures. Passing the desired arity explicitly via curryN fixes both cases and gives TypeScript a chance to type the chain end-to-end. The args.slice(0, arity) clamp also guarantees that the wrapped function never receives more than the declared arity, which matches what most curry users expect. Use this version any time the target function uses rest, defaults, or destructured parameters.
