Currying & partial application
Learn how partial application, currying, chainable calls, and point-free style reshape JavaScript functions for reuse without hiding the important trade-offs.
- 01Pre-fill argumentsBuild smaller functions with closures, bind, and a generic partial helper.
- 02Read curried callsTrace when
curry(fn)returns another function and when it finally runsfn. - 03Use the style honestlySpot arity traps, coercion tricks, and point-free callbacks whose arguments do not line up.
Change a function’s shape
JavaScript functions are flexible values. You can store them in variables, pass them to map, return them from other functions, and attach methods like bind. Currying and partial application use that flexibility to change when a function receives its arguments.
The job stays ordinary. Add tax. Format money. Sort products. Build a URL. The shape changes so repeated details can be filled in once and reused later.
Partial application saves some arguments now and returns a function for the missing arguments. Currying reshapes a multi-argument function into staged calls. Point-free style passes functions around without naming every temporary argument.
| Pattern | What changes | Tiny example |
|---|---|---|
| Partial application | Some arguments are fixed now; the returned function waits for the rest. | addTax(0.08)(50) or multiply.bind(null, 2)(7) |
| Currying | A multi-argument function is reshaped into one-argument stages. | curry(add3)(1)(2)(3) |
| Point-free style | You pass a function directly instead of naming every temporary value. | items.map(format) |
| Chainable sum trick | A function keeps returning functions and uses coercion or a terminator to reveal the total. | sum(1)(2)(3) + 0 or sum(1)(2)(3)() |
This lesson assumes you know closures, because saved arguments live in closures, and call, apply & bind, because bind can save leading arguments. We will also touch functions are objects and object-to-primitive conversion for the chainable sum trick.
Partial application: fill a few blanks now
STEP THROUGHImagine a function as a form with blanks. A normal call fills every blank at once: priceWithTax(0.08, 50). Partial application fills one or more blanks now and gives you a new form with fewer blanks: const addEightPercent = addTax(0.08).
At a sandwich shop, choose bread first, then choose the filling later. Partial application saves an earlier choice so the next call only needs the remaining argument.
- In real life: Choose bread first
- In JavaScript: The rate or prefix is saved now
- In real life: Choose the filling later
- In JavaScript: The returned function still needs a price or item
- In real life: Use the same bread choice again
- In JavaScript: Many call sites can reuse the smaller helper
Where the analogy stops: A sandwich order is written down once. A partially applied JavaScript function is live code and may still depend on this.
The lab shows three versions. A closure saves rate. bind saves the first argument of multiply and fixes this at the same time. A generic partial helper stores any preset list and forwards the later list.
Partial application means filling in some arguments now and receiving a smaller function for the rest. Change the mode and predict the arity before stepping.
script
return Number((price * (1 + rate)).toFixed(2));}; const addEightPercent = addTax(0.08);console.log(addEightPercent.length);console.log(addEightPercent(50));Notice the arity detail. bind reduces length by the number of preset arguments. A rest-parameter helper like (...later) => ... has length 0, even when the human meaning is “one suffix remains.” Arity is a clue, not a contract.
Currying: one door at a time
INTERACTIVECurrying is stricter in shape. A curried function returns a function, which may return a function, until enough arguments have been collected. The original function runs at the last stage, not at the first stage.
Picture a hallway of doors. Each door asks for one key before it lets you reach the next door. The room at the end is the real result. Until the last door opens, you are still holding a function, not the answer.
- In real life: Door one asks for key 1
- In JavaScript: First call receives
a - In real life: Door two asks for key 2
- In JavaScript: Returned function receives
b - In real life: The final door opens the room
- In JavaScript: Enough arguments collected;
fn(...args)runs
Where the analogy stops: Real doors usually require exactly one key each. JavaScript curry helpers often allow grouped keys, such as curried(1, 2)(3), because they collect arrays of arguments.
Currying turns one multi-argument function into nested calls. Step through the exact point where the original function finally runs.
script
return function curried(...args) { if (args.length >= fn.length) { return fn(...args); } return (...more) => curried(...args, ...more); };} const add3 = (a, b, c) => a + b + c;const curriedAdd3 = curry(add3);console.log(curriedAdd3(1)(2)(3));This common helper uses fn.length. If the collected argument count is high enough, it calls the original function. Otherwise it returns another function that remembers the current arguments. That is why curry(add3)(1)(2)(3) and curry(add3)(1, 2)(3) can both end at 6.
The sharp edge: fn.length is not a mind reader
REAL OUTPUTIn JavaScript, a function’s length counts parameters before the first default value. It does not count rest parameters. That means a small curry helper can call too early when a function has defaults, or miss the meaning of a rest parameter entirely.
A curry helper based on fn.length sees only one parameter in each function here, so it stops earlier than the written parameters suggest.
Professional libraries solve this by asking for an explicit arity, designing data-last helpers carefully, or avoiding automatic currying for functions with defaults and rest parameters. You can still write curried functions by hand when the public shape matters: const between = (min) => (max) => (value) => value >= min && value <= max.
The famous sum(1)(2)(3) party trick
INTERACTIVEChainable sums are a fun way to prove functions can return functions forever. They are not the same as ordinary currying, because there is no fixed final arity. The chain needs a separate signal that says “stop and show the number.”
The chain is still a function. Template strings and + 0 ask it for a primitive value, so Symbol.toPrimitive returns the saved total.
The Symbol.toPrimitive version is surprising: `${sum(1)(2)}` and sum(1)(2) + 0 ask the function to become a primitive. A plain console.log(sum(1)(2)) may show a function representation instead; consoles display objects and functions in their own way. The empty-call version is less magical: the final () is the terminator.
Chainable sums teach closures and primitive conversion beautifully. In production, a normal reduce or a named builder with a clear .value() method is usually easier to search, type, and debug.
Point-free style: concise until arity bites
INTERACTIVEPoint-free style removes parameter names when they do not add information. The everyday rewrite is items.map((item) => format(item)) becoming items.map(format). It reads nicely when the function accepts exactly what the caller passes.
On a clear shopping list, you write an item once instead of repeating it. Point-free code does the same with functions. When the item is unclear, write the extra detail rather than guessing.
- In real life: Write ‘tea’ once on the list
- In JavaScript: Compose or pass existing helpers
- In real life: Do not repeat the item name
- In JavaScript: No temporary
itemparameter - In real life: Add details when the item is unclear
- In JavaScript: A callback with mismatched arity needs an adapter
Where the analogy stops: A shopping list has no hidden arguments. Code has stricter inputs: extra arguments with meaning can make concise style a bug.
The long form names item, which is friendly while learning and useful when you need to adapt arguments.
The classic trap is ["1", "2", "3"].map(parseInt). map passes (value, index, array). parseInt reads its second argument as the radix. So the index accidentally changes the base. The safe version is explicit: (value) => parseInt(value, 10).
const items = [" tea", "cake "];const format = (item) => item.trim().toUpperCase(); console.log(items.map((item) => format(item)).join(", "));console.log(items.map(format).join(", "));console.log(["1", "2", "3"].map(parseInt).join(", ")); const addTax = (rate) => (price) => price * (1 + rate);const formatMoney = (value) => "$" + value.toFixed(2);const withSalesTax = (price) => formatMoney(addTax(0.08)(price));console.log([10, 20].map(withSalesTax).join(", "));Where you’ll use this
SORT ITThese patterns shine when one option repeats across many calls: currency, locale, tax rate, validation rule, logging prefix, API base path, or sort direction. They are less helpful when the wrapper is used once and hides the real arguments.
| Use case | Fit | Why |
|---|---|---|
| Tax or formatting helpers | Good fit | Preset the rate, locale, prefix, or options once; use the smaller function in many places. |
| Array callbacks | Often useful | Point-free is clear when callback arguments match, like items.map(format). |
| Sorting by one option | Sometimes | A curried comparator factory such as byPrice("desc") reads well if the option is truly reusable. |
| Public API everyone must debug | Be careful | Too many tiny wrappers can hide the real arguments and allocate extra closures. |
Here partial application is useful because the direction is a reusable option. If you only sort once, an inline comparator is fine too.
toSorted so the original array is not mutated.const double = multiply.bind(null, 2);const addTax = (rate) => (price) => price * (1 + rate);products.toSorted((a, b) => a.price - b.price);const dollars = partial(label, "$");curry(add3)(1)(2)(3);items.map(format);
Sort each snippet by the pattern it actually uses. Point-free by itself belongs in neither.
Common misconceptions
- “Partial application and currying are the same.” They overlap, but partial application is about saving some arguments; currying is about staged function shape.
- “Point-free is always cleaner.” It is cleaner only when the callback’s arguments line up with the function’s parameters.
- “
fn.lengthtells me all required arguments.” It stops at defaults and ignores rest parameters. - “
bindonly changesthis.” It can also preset leading arguments, which is partial application. - “Curried functions are free.” Each stage usually allocates a closure. That is fine most of the time, but it is still real work.
- “Chainable sums are production style.” They are mostly a lesson in closures and coercion. Use clear APIs for teams.
| Question | Partial application | Currying | Point-free |
|---|---|---|---|
| What do I look for? | Some arguments are already saved. | Calls return more functions until enough arguments arrive. | The callback is passed directly without naming a parameter. |
| What can go wrong? | The wrapper hides arguments or this. | fn.length can stop too early. | Extra callback arguments can change meaning. |
| Good smell | A repeated option disappears from many call sites. | Small data-last helpers compose cleanly. | The shorter version reads exactly like the long version. |
Practice exercises
5 EXERCISESRead the code and type the number printed by the final line.
const addTax = (rate) => (price) => price * (1 + rate);
const addTenPercent = addTax(0.10);
console.log(addTenPercent(30));The rate is saved first. The final call computes 30 * 1.10, so the console prints 33.
The original point-free version is wrong. Change it so the output is 1,2,3.
const numbers = ["1", "2", "3"];
console.log(numbers.map(parseInt).join(","));numbers.map((value) => parseInt(value, 10))The adapter receives all map arguments but passes only the string and a fixed radix to parseInt.
Predict the exact text logged by the grouped curried call.
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length
? fn(...args)
: (...more) => curried(...args, ...more);
};
}
const join3 = (a, b, c) => a + "-" + b + "-" + c;
console.log(curry(join3)("A")("B", "C"));The helper collects A, then B and C. That reaches three arguments, so join3 returns A-B-C.
Type the number printed by the chain with an explicit terminator.
function sum(total) {
return (value) => value === undefined ? total : sum(total + value);
}
console.log(sum(4)(5)(6)());Each numbered call returns another function with a larger total. The last empty call returns 15.
A shop uses one tax rate across dozens of price displays. Should you reach for partial application, currying, or neither? Type the best pattern name.
const addStoreTax = addTax(0.0825);Pre-filling one tax rate for many later prices is partial application. You do not need a fully automatic curry helper unless the surrounding style already uses one.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which sentence best defines partial application?
Choose an answer to see the explanation.
Question 2 of 7What does the bound multiplier log?
Read the code, then predictfunction multiply(a, b) { return a * b; } const double = multiply.bind(null, 2); console.log(double(6));Choose an answer to see the explanation.
Question 3 of 7When does this curried call finally run
add3?Read the code, then predictconst add3 = (a, b, c) => a + b + c; console.log(add3.length);Choose an answer to see the explanation.
Question 4 of 7What does the default-parameter arity example print first?
Read the code, then predictconst withDefault = (a, b = 10, c) => a + b + c; console.log(withDefault.length);Choose an answer to see the explanation.
Question 5 of 7What does the primitive-chain sum log?
Read the code, then predictfunction sum(total) { function next(value) { return sum(total + value); } next[Symbol.toPrimitive] = () => total; return next; } console.log(sum(1)(2)(3) + 0);Choose an answer to see the explanation.
Question 6 of 7Why is
['1','2','3'].map(parseInt)a trap?Read the code, then predictconsole.log(["1", "2", "3"].map(parseInt).join(","));Choose an answer to see the explanation.
Question 7 of 7Which point-free rewrite is safe here?
Read the code, then predictconst format = (item) => item.trim().toUpperCase(); const items = [" tea "]; console.log(items.map(format).join(""));Choose an answer to see the explanation.
Key takeaways
- Partial application presets some arguments and returns a smaller function.
- Currying collects arguments across staged calls and runs the original function later.
fn.lengthis useful but breaks down with defaults and rest parameters.- Chainable sums are great for learning closures and coercion, not a default API style.
- Point-free style works only when callback arguments match the function’s parameters.
Remember the one-liner.
Use currying and partial application to make reusable smaller functions, but keep the arguments visible when clarity or arity demands it.
Up next: Composition & pipelines.