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.
- 01Give some arguments nowMake a smaller function that saves the first arguments, using a closure,
bind, or apartialhelper. - 02Follow a curried callSay when
curry(fn)returns another function and when it finally runsfn. - 03Spot the trapsNotice when
fn.lengthcounts too few, howsum(1)(2)(3)gets its number, and whenmap(fn)passes argumentsfndoesn't expect.
Give a function its arguments in steps
In JavaScript, a function is a value, like a number or a string. You can keep it in a variable. You can pass it to map. You can return it from another function.
This lesson uses that to change when a function gets its arguments. The job stays the same: add tax, format a price, sort products. But you give one argument now and the rest later. That way, a setting you use again and again, like a tax rate, is written only once.
This lesson covers three ways to shape a function:
- Partial application: give some arguments now, and get back a function that takes the rest.
- Currying: turn a function that takes several arguments into a chain of calls, one call per argument.
- Point-free style: pass a function by its name, like
items.map(format), instead of wrapping it in a new function.
The table below shows each one with a short example. Read each row as: "this pattern does this, like in this code." The last row is a puzzle you'll meet later in the lesson.
| Pattern | What it does | Short example |
|---|---|---|
| Partial application | Gives some arguments now. The function you get back waits for the rest. | addTax(0.08)(50) or multiply.bind(null, 2)(7) |
| Currying | Turns a function with several parameters into a chain of calls, one per argument. | curry(add3)(1)(2)(3) |
| Point-free style | Passes a function by its name, with no wrapper that names the value. | items.map(format) |
| The sum chain puzzle | Keeps returning functions. A conversion or an empty call () gives the total. | sum(1)(2)(3) + 0 or sum(1)(2)(3)() |
This lesson builds on two earlier ones. Closures explains how a function remembers variables from the place it was made; saved arguments are kept that way. call, apply & bind explains bind, which can save the first arguments of a function. The sum(1)(2)(3) puzzle also uses ideas from functions are objects and object-to-primitive conversion.
Partial application: give some arguments now
STEP THROUGHThink of a function as a form with blanks. A normal call fills in every blank at once: priceWithTax(0.08, 50). Partial application fills in some blanks now. You get back a new form with fewer blanks.
Here is the shape. addTax takes a rate and returns a new function. That new function takes a price. So const addEightPercent = addTax(0.08) fills in the rate. Later, addEightPercent(50) fills in the price and gives 54.
At a sandwich shop, you choose the bread first. The person behind the counter remembers it. Then you choose the filling. You don't have to say the bread again.
- In real life: You choose the bread first
- In JavaScript:
addTax(0.08)gets the rate now - In real life: You choose the filling later
- In JavaScript:
addEightPercent(50)gets the price later - In real life: The same bread for many sandwiches
- In JavaScript:
addEightPercentworks for many prices
The step-through below shows three ways to save the first arguments:
- Closure:
addTaxreturns a function that remembersrate. A function that remembers variables from the place it was made is called a closure. - bind:
multiply.bind(null, 2)makes a new function. The first argument tobind,null, setsthis. The next one,2, is saved as the first argument ofmultiply. - Partial helper: a small
partialfunction that saves any number of first arguments, and puts the later ones after them.
Partial application means giving some arguments now and getting back a smaller function for the rest. Pick a way, guess the length it prints, then press Step.
script
return Number((price * (1 + rate)).toFixed(2));}; const addEightPercent = addTax(0.08);console.log(addEightPercent.length);console.log(addEightPercent(50));Watch the lines that print length. Every function has a length: the number of parameters it lists. A function made by bind lists one less for each saved argument.
The partial helper returns (...later) => .... Here ...later is a rest parameter: it collects all the arguments into an array. A rest parameter isn't counted, so length is 0, even though a suffix is still needed. So length is a hint, not a promise.
Currying: one call for each argument
INTERACTIVECurrying changes how you call a function that takes several arguments. Instead of add3(1, 2, 3), you write curriedAdd3(1)(2)(3). That is three calls in a row:
- Each call saves its argument and returns another function.
- When all the arguments have arrived, the original function runs and gives the result.
Until the last call, you are holding a function, not the answer.
Picture a hallway with three doors in a row. Each door asks for one key. When you give it, you reach the next door. Behind the last door is the room you wanted.
- In real life: The first door takes key 1
- In JavaScript: The first call gets
1and returns a function - In real life: The second door takes key 2
- In JavaScript: That function gets
2and returns another one - In real life: The last door opens the room
- In JavaScript: All three arguments are in, so
add3runs
The step-through uses a small curry helper. Here is what each part does:
curry(fn)returns a function calledcurried. It collects the arguments it gets intoargs, with a rest parameter.- Line 3 checks: are there at least
fn.lengtharguments? Foradd3, that's 3. - If yes, line 4 runs
fnwith all of them. - If not, line 6 returns a new function. It waits for
morearguments and adds them to the saved ones.
Currying lets you give a function its arguments over several calls. Step through to find the exact call where add3 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));add3 runs.The helper only counts arguments, so you can also give them in groups. curriedAdd3(1)(2)(3) and curriedAdd3(1, 2)(3) both reach three arguments, and both print 6.
When fn.length counts too few
REAL OUTPUTThe curry helper trusts fn.length. But length doesn't always count every parameter:
- It counts from the left and stops at the first parameter with a default value, like
b = 10. - It never counts a rest parameter, like
...values.
length counts parameters from the left and stops at the first one with a default value. So it says 1, not 3.So the helper can run a function too early. The code below shows both cases. Each function has a length of 1, so curry runs it after just one argument.
Both functions here have a length of 1. So a curry helper that trusts fn.length runs each one after just one argument.
Look at line 12. curry(withDefault)(1) already ran withDefault, with c still missing. The result is NaN ("not a number"), so typeof prints number, not function. Line 14 also runs at once, so curry(withRest)("id") prints just id.
There are two common fixes:
- Tell the helper the count yourself, as in the code below.
- Write the curried function by hand, with one arrow for each argument:
const between = (min) => (max) => (value) => value >= min && value <= max.
function curryN(fn, arity) { return function curried(...args) { if (args.length >= arity) return fn(...args); return (...more) => curried(...args, ...more); };} const withDefault = (a, b = 10, c) => a + b + c;const addThreeValues = curryN(withDefault, 3);console.log(addThreeValues(1)(2)(3));curryN takes the count as a second argument, here 3. It doesn't look at fn.length, so the default on b can't make it stop early. It collects all three values, 1, 2, and 3, and prints 6. The cost: if you change the function's parameters later, you must change the count too.
The sum(1)(2)(3) puzzle
INTERACTIVEA well-known puzzle asks you to write a sum that works like sum(1)(2)(3). Each call returns a function that remembers the running total.
This is not normal currying. A curried function knows how many arguments to wait for. A chain like this could go on forever. So it needs a separate signal that says "stop now and give me the number". The lab shows two signals:
- Convert it to a plain value. A function is an object, so it can have extra properties.
Symbol.toPrimitiveis a special property name. When JavaScript needs to turn an object into a plain value, like a number or a string, it calls the method stored there. - End with an empty call. A call with no argument,
(), returns the total.
The chain is still a function. A template string and + 0 both ask it for a plain value. So JavaScript calls Symbol.toPrimitive, which returns the saved total.
In the first version, `${sum(1)(2)}` is a template string, so it needs a string. And sum(1)(2) + 0 needs a plain value to add. Both make JavaScript call Symbol.toPrimitive, which returns the saved total.
A plain console.log(sum(1)(2)) doesn't ask for a plain value, so the console may show the function instead. The second version is easier to follow: the last () ends the chain.
This puzzle is good practice for closures and conversion. In real code, use something clearer: a normal reduce that adds up an array, or an object with a named method like .value(). Those are easier to search for and to debug.
Point-free style: pass the function itself
INTERACTIVELook at items.map((item) => format(item)). The arrow function only takes item and hands it to format. You can drop that wrapper and write items.map(format). This is called point-free style: you don't name the value, item, that passes through.
It is safe when the function expects exactly what the caller passes. That needs care, because map passes three arguments to its function, not one: the value, its index, and the whole array.
A shopping list says "tea". It doesn't say "take an item, and if the item is tea, buy that item". The short form works because everyone reads it the same way. If a note could be read two ways, like "2 milk", you add the detail: "2 cartons of milk".
- In real life: Write "tea" on the list
- In JavaScript: Pass the function by name:
items.map(format) - In real life: Skip "take an item and buy that item"
- In JavaScript: No extra
itemparameter to name - In real life: Write "2 cartons of milk", not "2 milk"
- In JavaScript: Write the wrapper when the arguments don't match
The long form names item and hands it to format. Writing it out helps when you need to choose which arguments go in.
parseInt trap differ in one thing: whether the arguments from map match what the function expects.The best-known trap is ["1", "2", "3"].map(parseInt). parseInt turns a string into a whole number. Its second argument is the radix, the number base: 10 for everyday numbers, 2 for binary. map passes the index as that second argument. So the index changes the base by accident:
parseInt("1", 0): base 0 means "choose the base for me", so this gives 1.parseInt("2", 1): there is no base 1, so this givesNaN.parseInt("3", 2): in base 2 the only digits are 0 and 1, so this givesNaN.
map passes the index, 1, and parseInt reads it as the number base. The wrapper always passes 10.The fix is to write the wrapper: (value) => parseInt(value, 10). It takes only the value and always passes 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(", "));Lines 4 and 5 print the same thing, TEA, CAKE. Line 6 is the trap and prints 1, NaN, NaN. Lines 8 to 11 build prices from small curried helpers. toFixed(2) writes a number with two digits after the point, so line 11 prints $10.80, $21.60.
Where you'll use this
SORT ITThese patterns help most when the same setting appears in many calls. For example:
- a currency, a language, or a tax rate
- a rule that checks a form field
- a prefix for log messages, or the start of an API address
- a sort direction
They help less when the smaller function is used only once. Then it just hides the real arguments.
const formatPrice = (currency) => (price) => currency + price.toFixed(2); const dollars = formatPrice("$");console.log(dollars(12));console.log(dollars(4.5));Line 1 takes the currency symbol and returns a function that still needs a price. Line 2 is what that function returns. toFixed(2) writes the price with two digits after the point. Line 4 makes the dollar formatter once. Lines 5 and 6 use it for two prices and print $12.00 and $4.50.
The price, the part that changes, comes last. So you could also pass dollars straight to map.
const mapWith = (transform) => (items) => items.map(transform);const upper = (item) => item.toUpperCase();const upperItems = mapWith(upper); console.log(upperItems(["tea", "cake"]).join(", "));Line 1 makes mapWith. It takes the job first, transform, and the list last, items. Line 2 makes upper, which turns a word into capital letters. Line 3 fills in the job: mapWith(upper) returns a function that waits for a list. Line 5 gives it ["tea", "cake"] and prints TEA, CAKE.
This order is called data-last: settings first, changing data last. It isn't a rule of the language. But it gives your small helpers the same shape, so they are easy to join together.
const makeButtonHandler = (action) => (event) => action + ":" + event.currentTarget.dataset.id; const openSettings = makeButtonHandler("open");console.log(openSettings({ currentTarget: { dataset: { id: "settings" } } }));A factory is a function that makes other functions. makeButtonHandler takes an action now and returns a click handler. A page can call makeButtonHandler("open") once and attach the result to a button.
When the button is clicked, the browser calls the handler with an event object. event.currentTarget is the button, and dataset.id reads its data-id attribute. Line 5 passes a small object with the same shape instead of a real click, so you can see the output: open:settings.
The table below sums up where these shapes fit. Read each row as: "in this kind of code, is it a good fit, and why?"
| Where it shows up | Is it a good fit? | Why |
|---|---|---|
| Tax or price formatting | Yes, a good fit. | You give the rate, language, or symbol once, then use the smaller function in many places. |
Functions passed to map or filter | Often. | Point-free is clear when the arguments match, like items.map(format). |
| Sorting by one setting | Sometimes. | byPrice("desc") reads well if you sort the same way in many places. |
| Code that many people must debug | Be careful. | Many tiny wrappers hide the real arguments, and each one is an extra function to create. |
Sorting is a common place to choose. In the code below, byPrice takes a direction and returns a compare function. A compare function gets two items, a and b. It returns a negative number if a goes first, or a positive number if b goes first.
byPrice("desc") saves the direction and returns a compare function for toSorted. This helps when you sort the same way in many places. If you sort only once, a compare function written inline is fine.
toSorted returns a new sorted array, so products stays as it was.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 line of code by the pattern it uses. Point-free style on its own goes in Neither.
Common mix-ups
- "Partial application and currying are the same." They often appear together, but they are different. Partial application saves some arguments. Currying turns a function into a chain of calls, one per argument.
- "Point-free is always cleaner." It is cleaner only when the arguments the caller passes match the function's parameters.
map(parseInt)is where it goes wrong. - "
fn.lengthcounts all the parameters." It stops at the first default value and skips rest parameters. - "
bindonly changesthis." It can also save the first arguments. That is partial application. - "Curried functions cost nothing." Each call in the chain usually makes a new function. That is fine almost always, but it is still work for the computer.
- "Chainable sums are a good pattern for real code." They are mostly a puzzle about closures and conversion. Teams find plain functions easier to read.
The table below puts the three patterns side by side. Each column is one pattern. Each row is a question you can ask about a piece of code.
| Question | Partial application | Currying | Point-free |
|---|---|---|---|
| What do I look for? | Some arguments are already saved. | Each call returns another function until all the arguments arrive. | A function is passed by name, with no wrapper around it. |
| What can go wrong? | The smaller function hides which arguments, or which this, it uses. | fn.length can count too few, so the function runs too early. | Extra arguments from the caller, like the index from map, change the result. |
| What does it look like when it helps? | A setting that was repeated in many calls is now written once. | Small helpers that take the data last join together easily. | The short version does exactly what the long version did. |
Practice exercises
5 EXERCISESRead the code. Type the number the last line prints.
const addTax = (rate) => (price) => price * (1 + rate);
const addTenPercent = addTax(0.10);
console.log(addTenPercent(30));The rate is saved first. The last call works out 30 * (1 + 0.10), which is 33, so the console prints 33.
This short form prints the wrong numbers. Change it so it prints 1,2,3.
const numbers = ["1", "2", "3"];
console.log(numbers.map(parseInt).join(","));numbers.map((value) => parseInt(value, 10))The wrapper gets all the arguments from map, but passes only the string and a base of 10 to parseInt.
Type the exact text the last line prints.
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 saves A, then gets B and C. That makes three arguments, so join3 runs and returns A-B-C.
Type the number this chain prints.
function sum(total) {
return (value) => value === undefined ? total : sum(total + value);
}
console.log(sum(4)(5)(6)());sum(4) starts the total at 4. The call with 5 makes it 9, the call with 6 makes it 15, and the empty call returns 15.
A shop uses one tax rate in dozens of places that show prices. Which would you use: partial application, currying, or neither? Type the name of the pattern.
const addStoreTax = addTax(0.0825);You save one tax rate now and give many prices later. That is partial application. You don't need a full curry helper unless the code around it already uses one.
Check your understanding
7 QUESTIONSQuestion 1 of 7Which sentence describes partial application?
Choose an answer to see the explanation.
Question 2 of 7What does this
bindexample print?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 7This code prints
add3.length. With the lesson'scurryhelper, when doescurry(add3)runadd3?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
withDefault.lengthprint?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 this
sumchain print?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 7What does
map(parseInt)print here?Read the code, then predictconsole.log(["1", "2", "3"].map(parseInt).join(","));Choose an answer to see the explanation.
Question 7 of 7What does
items.map(format)print 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 gives some arguments now and returns a smaller function for the rest.
- Currying lets you give the arguments over several calls. The original function runs when all of them have arrived.
fn.lengthstops at the first default value and skips rest parameters, so acurryhelper can run a function too early.- The
sum(1)(2)(3)chain is a good puzzle for learning closures and conversion, not a pattern for everyday code. - Point-free style is safe only when the caller's arguments match the function's parameters.
Remember the one-liner.
Use currying and partial application to make small, reusable functions. Write the arguments out when that is clearer, or when the counts don't match.
Up next: Composition & pipelines, where you join small functions like these into longer chains.