Variadic curry() — Call with Any Number of Args

The hardest curry variant: transform f(a, b, c, d) so it can be called as f(a)(b, c)(d) — any number of arguments at each step, accumulating until enough args are collected.

12 min read
JavaScript
Interview
Implementation
Functional

TABLE OF CONTENTS

The hardest curry variant: transform f(a, b, c, d) so it accepts any number of arguments at each call — f(a)(b, c)(d) — and invokes the original when enough args have been collected. It's curry without the one-arg-at-a-time restriction.

Related deep-dive: Functions #2 — Currying & Partial Application


What is Variadic curry()?

Variadic curry(fn) is the generalization of the fixed-arity curry from Interview #11. Instead of requiring exactly one argument per call — curried(a)(b)(c) — the variadic version accepts any number of arguments at each call: curried(a, b)(c), curried(a)(b, c), or even curried(a, b, c). It invokes fn as soon as the total accumulated arguments reaches or exceeds fn.length.

The core mechanism is the same as fixed-arity curry: recursive argument accumulation with fn.length as the termination condition. The difference is that each call collects ...args (any number) rather than a single argument, and all collected args are spread onto the accumulator.

The termination condition totalArgs.length >= fn.length is what makes this "variadic" — you can provide arguments in any-sized batches. Once the total meets or exceeds the function's declared parameter count, the function executes. Excess arguments beyond fn.length are still passed through (matching native behavior).

Real-world use cases:

  • Flexible partial applicationconst greetHello = curry(greet)("Hello") then later greetHello("Alice") or greetHello("Bob")
  • Configuration pipelines — pre-configure a function with config in one call, then pass the data in another: const configured = curry(process)(config); configured(data)
  • Event handlersconst handler = curry(handleEvent)(eventType)(metadata) then attach handler(payload) to the actual event

The variadic version is the one that matches how Lodash's _.curry works and is the version most useful in practice. The fixed-arity version is primarily an interview stepping stone.


The Problem

"Implement curry(fn) where the curried function can be called with any number of arguments at each step. It should invoke fn as soon as the total accumulated arguments reaches or exceeds fn.length."


Thought Process

The pattern from Interview #11 (fixed-arity curry) only allows one argument at a time. Here we need to:

  1. Accumulate args across calls
  2. Check after each call whether we have enough
  3. If yes, invoke fn with the accumulated args
  4. If no, return a new function that continues accumulating

The key difference from #11: instead of returning (nextArg) => ..., we return (...nextArgs) => ... — allowing any number of arguments per call.


Step 1 — Base Variadic Curry

Loading editor...


Step 2 — Handling Extra Arguments

The interviewer asks: "What if you pass more arguments than fn expects?"

Loading editor...

Some implementations pass all args to fn and let the function decide. Others slice to fn.length. State your choice and why.


Step 3 — Preserving this Context

Loading editor...


Step 4 — Functions with Rest Parameters or Defaults

The interviewer: "What about fn.length for functions with rest params?"

Loading editor...

Key insight: Functions with rest params (...args) have fn.length === 0. Similarly, functions with default params only count parameters before the first default. If the interviewer gives you such a function, ask: "Should I accept an explicit arity?"


Step 5 — Infinite Currying with Terminator

A related variant: infinite currying where the function is called with no arguments to terminate:

Loading editor...

Note: this variant accumulates args in a closure across all call chains (the args array is shared). For a fresh accumulator each time, re-create the curried function.


Edge Cases

fn.length = 0 (no-arg function): curry(() => 'hello')() — the first call has args.length >= 0, so it invokes immediately. Correct.

Passing more args than expected: Either slice to fn.length or pass all. State your choice.

Calling with no arguments when arity isn't met: Return a new function that waits for args. Don't invoke fn prematurely.


Full Solution

Loading editor...


What Interviewers Are Testing

  • Accumulation pattern — gathering arguments across multiple calls
  • Arity detection — using fn.length to know when to invoke
  • Rest parameter handling — understanding that fn.length doesn't count rest params
  • Explicit arity override — letting the caller specify arity when fn.length is unreliable
  • this forwarding — preserving context through all call levels

Complexity

TimeSpace
create curriedO(1)O(1)
each callO(1) — spread + concatO(N) — accumulated args

Interview Tips

  • Start from the fixed-arity curry — "This is the same pattern as fixed-arity curry, but (arg) becomes (...args). That's the only change at the base level."
  • Ask about fn.length edge cases early — "How should I handle functions with rest parameters or default values? Should I accept an explicit arity parameter?"
  • Show infinite curry as a variant — "There's also infinite curry where you call with () to terminate. That uses a shared accumulator rather than arity-checking."
  • Mention this forwarding — use .apply(this, ...) even if the test cases don't use this. It shows real-world awareness.

Related Questions


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI