Currying in JavaScript: When It Makes Sense (and When It's Over-Engineering)
Currying transforms multi-argument functions into chains of single-argument functions. Learn when this technique is powerful and when it's overkill.
Have you ever seen code like this: createLogger('INFO')('App')('Server started') and thought "what the hell is that"? That's currying, and when used well, it's one of the most elegant functional programming techniques. The problem is that many people reach for it without need and turn simple code into something unreadable.
Let's demystify it.
What Is Currying?
Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument:
The magic isn't in calling f(1)(2)(3) instead of f(1, 2, 3). The magic is being able to call f(1) and save the result for later.
Mental model: a regular function is a bulk delivery. You hand over all the boxes at once, you get the result. Currying is a nested gift. You unwrap one box, find another inside, unwrap that, and so on. Each unwrap remembers what was in the previous box (closure), so by the time you open the last one, every argument you ever passed is still there waiting.
Generic Curry: The Utility Function
You don't need to manually write each curried function. A helper handles it:
Currying vs Partial Application
This is where almost everyone trips. They look similar, they're not the same.
Currying transforms f(a, b, c) into a chain where each step takes exactly one argument. It's a structural transformation of the function itself.
Partial application takes a function and pre-fills some of its arguments, returning a new function that expects the rest. No rule about how many at a time.
Rule of thumb: every curried function enables partial application, but not every partial application is currying.
Where Currying Really Shines
Configurable Logger
The classic example, and for good reason. You configure once and reuse:
You build a specialized function out of a generic one. That's composition.
Composable Validators
Transformation Pipelines
Currying Is Already in Code You Use
You've probably shipped curried code without noticing:
- Ramda and lodash/fp: functions are auto-curried by default, and arguments are arranged data-last.
R.map(fn)(list)andR.map(fn, list)both work. - Redux Thunk: a thunk is
dispatch => getState => { ... }, a curried signature that lets the middleware injectdispatchfirst and hand yougetStateon demand. - styled-components:
styled.div`...`returns a component that receives props, then renders. Tagged templates plus a curried-style API under the hood. - React
useCallback/useMemo: the factory pattern(id) => useCallback(() => handle(id), [id])is currying to bind an argument into the closure. - Express-style middleware factories:
authorize('admin')(req, res, next)configures the check once, applies it many times.
When You Should NOT Curry
Currying is a tool. Sometimes it's the wrong one:
- Object methods that rely on
this.user.savecurried loses its receiver. Either use.bind()or keep it plain. - Hot loops and perf-critical paths. Each curry level creates a closure and an extra call. Tight inner loops (thousands of iterations per frame, game engines, heavy data crunching) pay a measurable cost.
- Teams unfamiliar with FP. If the reviewer has to Google
a => b => c => ..., you're creating friction, not elegance. - Functions you'll always call with every argument at once. If
f(a, b, c)never becomesf(a)saved somewhere, currying is cosplay. - Variadic functions (
...args). The genericcurryrelies onfn.length, which ignores rest params. It'll never resolve.
The Pitfalls of Currying
- Currying is not partial application. Currying applies one argument at a time. Partial application can apply several.
fn.lengthdoesn't count rest params. If your function uses...args, the generic curry breaks.- Debugging gets harder. Stack traces with nested closures read cryptic.
- Loss of
this. Object methods don't play well with curry. Use.bind()if needed. - Over-engineering. Not every function needs to be curried. If you're never going to partially apply, don't curry.
FAQ
How do I type a curried function in TypeScript?
For fixed arity, just chain: (a: number) => (b: string) => boolean. For a generic curry helper, you need recursive conditional types (Curry<F> utilities exist in libraries like ts-toolbelt). Most teams type the specific curried signature by hand, it's clearer.
Are there good autocurry libraries?
Yes. Ramda and lodash/fp curry everything by default. Rambda is a lighter, faster alternative with a smaller bundle and a near-identical API. For one-off needs, a 5-line curry helper (like the one above) beats pulling in a dependency.
What's the performance overhead? Measurable but usually negligible. Each extra call creates a closure and a function invocation. For normal app code, invisible. For hot loops doing millions of ops, benchmark before currying.
Is an arrow chain (a) => (b) => ... really currying?
Yes, that's manual currying. It's the most explicit form. The curry(fn) helper just automates writing that chain and lets you pass multiple args per call.
How does currying differ from bind()?
bind() does partial application (you can pre-fill any number of args) and binds this. Currying creates a new argument-by-argument structure and doesn't touch this. Overlapping problems, different solutions.
Key Takeaways
Currying is powerful for building specialized functions out of generic ones: configurable loggers, reusable validators, transformation pipelines. But it's a tool, not a dogma.
If you write f(a)(b)(c) and never use f(a) separately, you're adding complexity with no gain. Curry when partial application is the actual goal, not as decoration.