Understand JavaScript Currying in 7 Minutes

Understand JavaScript Currying in 7 Minutes

10 mins read
Oct 31, 2025
Share
editor-page-cover
Content
What is currying?
Ways to implement currying
Currying tutorial
Curry’s use of REST syntax
How does curry use .length()?
Next recursive pass
Combining past parameters
Currying vs. Partial Application: What’s the difference?
TypeScript-friendly Curry: Safe, typed implementations
Caveats & gotchas: Function.length, default / rest params, and more
Compose / Pipe + Currying: Real-world utility
Real-world use cases: Where currying shines
HTTP client wrappers
Validation pipelines
UI event handlers
Services: Advanced types & behaviors
Ingress vs Gateway API: The modern networking layer
Deployment strategies & rollout safety
Ensuring pod health & high availability
Choosing the right workload kind
Day-2 operations & debugging
Wrapping up
Continue reading about Functional programming and JavaScript

Currying is a great tool that allows you to split arguments into individual functions. The main advantage is that it allow you to specialize or partially apply functions to then be passed to higher order functions, like map() and reduce().

Today, we’ll show you how to use currying in your own programs.


Master Functional Programming

Learn functional programming with hands-on practice.

Functional Programming Patterns With RamdaJS



What is currying?#

The definition of currying is:

Currying turns multi-argument functions into unary (single argument) functions.

Curried functions take many arguments one at a time.

In other words, In other words, the function takes the first argument and returns a new function. This new function takes the second argument and returns a new function which then takes the third argument. This repeats until all arguments have been fulfilled.

Currying is helpful for functional programming as it allows a short hand syntax for this common functional procedure.

Let’s see an example:

C++
greet = (greeting, first, last) => `${greeting}, ${first} ${last}`;
greet('Hello', 'Bruce', 'Wayne'); // Hello, Bruce Wayne

Properly currying greet gives you:

C++
curriedGreet = curry(greet);
curriedGreet('Hello')('Bruce')('Wayne'); // Hello, Bruce Wayne

This 3-argument function has been turned into three unary functions. Currying returns a function for each passed parameter.


Ways to implement currying#

I say “properly currying” because some curry functions are more flexible in their usage. Currying’s great in theory, but invoking a function for each argument gets tiring in JavaScript.

Ramda’s curry function lets you invoke curriedGreet like this:

C++
// greet requires 3 params: (greeting, first, last)
// these all return a function looking for (first, last)
curriedGreet('Hello');
curriedGreet('Hello')();
curriedGreet()('Hello')()();
// these all return a function looking for (last)
curriedGreet('Hello')('Bruce');
curriedGreet('Hello', 'Bruce');
curriedGreet('Hello')()('Bruce')();
// these return a greeting, since all 3 params were honored
curriedGreet('Hello')('Bruce')('Wayne');
curriedGreet('Hello', 'Bruce', 'Wayne');
curriedGreet('Hello', 'Bruce')()()('Wayne');

Notice you can choose to give multiple arguments in a single shot. This implementation’s more useful while writing code.

And as demonstrated above, you can invoke this function forever without parameters and it’ll always return a function that expects the remaining parameters.

Mr. Elliot shared a curry implementation much like Ramda’s. Here’s the code, or as he aptly called it, a magic spell:

C++
const curry = (f, arr = []) => (...args) =>
((a) => (a.length === f.length ? f(...a) : curry(f, a)))([...arr, ...args]);

It’s incredibly concise, so let’s refactor and appreciate it together.


The version below works the same as the dense implementation above. I’ve also sprinkled debugger statements to examine it in Chrome Developer Tools.

C++
curry = (originalFunction, initialParams = []) => {
debugger;
return (...nextParams) => {
debugger;
const curriedFunction = (params) => {
debugger;
if (params.length === originalFunction.length) {
return originalFunction(...params);
}
return curry(originalFunction, params);
};
return curriedFunction([...initialParams, ...nextParams]);
};

Open your Developer Tool sand follow along!


Currying tutorial#

To get started, paste greet and curry into your console. Then enter curriedGreet = curry(greet) to begin.

widget

Inspecting our two parameters we see originalFunction is greet and initialParams defaulted to an empty array because we didn’t supply it.

curry(greet) just returns a new function that expects 3 more parameters. Type curriedGreet in the console to see the the new function.

Now that we’ve seen the basic function, let’s get a bit more advanced and dosayHello = curriedGreet('Hello').

widget

To see the original function and parameters, type originalFunction and initialParams in your console.

Notice we can still access those 2 parameters even though we’re in a completely new function? That’s because functions returned from parent functions enjoy their parent’s scope.

After a parent functions can leave their parameters for their child functions to use. Kind of like inheritance in the real life sense.

curry was initially given originalFunction and initialParams and then returned a “child” function.

Those 2 variables are not disposed in case the child function needs to reference them. If they don’t, then that scope gets cleaned up and the parameters are deleted.


Curry’s use of REST syntax#

On line 4, put a debugger to check the status of the program. This will help us understand how curry handles parameters.

widget

Inspect nextParams and see that it’s ['Hello']…an array? But I thought we said curriedGreet(‘Hello’) , not curriedGreet(['Hello'])!

Correct: we invoked curriedGreet with 'Hello', but thanks to the REST syntax, 'Hello' becomes into ['Hello'].

This is because curry is a general function that can be supplied 1, 10, or 10,000,000 parameters, so it needs a way to reference all of them. Using the REST syntax like that captures every single parameter in one array, making curry’s job much easier.

Let’s jump to the next debugger statement.

You may have noticed that line 12 actually ran before the debugger statement on line 6. Our program defines a function called curriedFunction on line 5, uses it on line 12, and then we hit that debugger statement on line 6.

What is curriedFunction invoked with?

C++
[...initialParams, ...nextParams];

Look at params on line 5 and you’ll see ['Hello']. Both initialParams and nextParams were arrays, so we flattened and combined them into a single array using the handy spread operator (...).

Take a look:

widget

Line 7 says “If params and originalFunction are the same length, call greet with our params and we’re done.”


How does curry use .length()?#

This is how curry does its magic! Curry uses the output from .length() to know when it must ask for more parameters.

In JavaScript, a function’s .length property tells you how many arguments it expects. If there are less arguments (lower length) than expected, curry will ask for more arguments.

C++
greet.length; // 3
iTakeOneParam = (a) => {};
iTakeTwoParams = (a, b) => {};
iTakeOneParam.length; // 1
iTakeTwoParams.length; // 2

If our provided and expected parameters match, we’re good, just hand them off to the original function and finish the job!

Our example has less than the desired number of arguments.

We only provided ‘Hello’, so params.length is 1, and originalFunction.length is 3 because greet expects 3 parameters: greeting, first, last.

Since that if statement evaluates to false, the code will skip to line 10 and re-invoke our master curry function. It re-receives greet and this time, 'Hello', and begins all over again.

curry is essentially an infinite recursive loop of self-calling, parameter-hungry functions that won’t rest until the function has enough parameters.

widget

Next recursive pass#

Same parameters as before, except initialParams is ['Hello'] this time. Skip again to exit the cycle. Type our new variable into the console, sayHello. It’s still expecting more parameters but we’re getting closer to our exit condition.

Next we’ll try it with sayHelloToJohn = sayHello('John').

We’re inside line 4 again, and nextParams is ['John']. Jump to the next debugger on line 6 and inspect params: it’s ['Hello', 'John']!

widget

Combining past parameters#

Line 12 says “Hey curriedFunction, he gave me 'Hello' last time and ‘John’ this time." It then combines both into this array using the spread operator, [...initialParams, ...nextParams].

widget

Now curriedFunction again compares the length of these params to originalFunction, and since 2 < 3 we move to line 10 and call curry once again! And of course, we pass along greet and our 2 params, ['Hello', 'John'].

widget

Next we pass the final parameter:

sayHelloToJohnDoe = sayHelloToJohn('Doe')

This is added to the array with the previous parameters. By this point, we have the 3 we need and our exit condition triggers.

widget
widget
widget
widget

Currying vs. Partial Application: What’s the difference?#

Many developers mix up currying and partial application — they are related but distinct concepts.

Currying transforms a function that takes multiple arguments into a sequence of unary (one-argument) functions:

f(a, b, c) → f(a)(b)(c)

Partial application fixes some arguments of a function, returning a new function with fewer parameters:

g = f(a, b) → g(c)

Currying

unary steps

Composability

Partial

bind some args

Convenience / reuse

Example:

function add(a, b, c) { return a + b + c; }
const curried = curry(add); // curried(1)(2)(3) → 6
const partial = add.bind(null, 1); // partial(2, 3) → 6

Emphasizing this distinction helps developers apply each technique correctly.

TypeScript-friendly Curry: Safe, typed implementations#

In modern codebases, you want to preserve type safety when currying.
TypeScript’s variadic tuple types make this possible:

function curry<
Fn extends (...args: any[]) => any,
P extends any[] = Parameters<Fn>,
R = ReturnType<Fn>
>(fn: Fn): CurryFn<P, R> {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args);
} else {
return (...rest: any[]) => curried(...args, ...rest);
}
} as any;
}

This approach allows TypeScript to infer argument types across successive calls, enhancing safety and developer experience.

Caveats & gotchas: Function.length, default / rest params, and more#

The blog’s reliance on fn.length to determine the original arity is fragile:

  • fn.length ignores rest parameters (e.g., (...args) => {} reports length 0).

  • Default parameters after the first default reduce .length as well.

  • Arrow vs normal functions may have differing behavior in some environments.

Because of these caveats, many curry implementations accept multiple arguments per call (not strictly unary) to be more forgiving.

Compose / Pipe + Currying: Real-world utility#

Currying becomes truly expressive when paired with compose or pipe to build function pipelines.

const trim = (s) => s.trim();
const toLower = (s) => s.toLowerCase();
const isLong = (min) => (s) => s.length >= min;
const check = pipe(trim, toLower, isLong(5));
// check(" HELLO ") → true

You can then use curried versions of isLong or other small predicates to build reusable logic blocks.

Real-world use cases: Where currying shines#

Here are examples of how currying is used in real applications:

HTTP client wrappers#

const req = curry((method, url, opts) => fetch(url, { method, ...opts }));
const getJSON = req('GET', '/api/data');

Validation pipelines#

const minLen = (n) => (s) => s.length >= n;
const isAlpha = (s) => /^[A-Za-z]+$/.test(s);
const validate = compose(minLen(3), isAlpha);

UI event handlers#

const setField = curry((name, value) => form[name] = value);
const onNameChange = setField('name');

These examples show that currying isn’t just theoretical — it helps build modular, reusable, and elegant code.

Services: Advanced types & behaviors#

Modern Kubernetes Services offer more than just exposing pods.
They provide advanced routing and networking features that help applications scale and perform reliably.

  • Service types:
    Use ClusterIP for internal communication, NodePort for exposing apps on each node,
    LoadBalancer for cloud-managed load balancers, and ExternalName for aliasing external resources.

  • Headless Services:
    Set clusterIP: None to directly expose pod DNS names — ideal for stateful apps like databases.

  • internalTrafficPolicy:
    Restrict traffic to node-local endpoints to reduce latency and cross-node traffic.

  • SessionAffinity:
    Enable sticky sessions for stateful web apps.

  • EndpointSlice:
    Modern service discovery mechanism replacing legacy Endpoints, improving scalability.

These options give you fine-grained control over service behavior and performance.

Ingress vs Gateway API: The modern networking layer#

Ingress was once the default for HTTP routing, but the Gateway API is now the future of Kubernetes networking.
It’s richer, more extensible, and better suited for modern, multi-team environments.

  • Ingress: Still great for simple host/path-based routing.

  • Gateway API: Provides HTTPRoute, TCPRoute, GRPCRoute, and traffic splitting.
    It supports delegation, cross-namespace routing, and multi-tenant use cases.

Example HTTPRoute:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: my-app-route
spec:
parentRefs:
- name: my-gateway
rules:
- matches:
- path:
type: PathPrefix
value: "/api"
backendRefs:
- name: my-service
port: 80

This richer model is quickly becoming the standard for Kubernetes networking.

Deployment strategies & rollout safety#

A simple deployment isn’t enough for production.
Use deployment strategies to ensure smooth updates and reduce risk.

  • RollingUpdate: Default strategy. Tune maxSurge and maxUnavailable for safer rollouts.

  • Blue/Green Deployments: Deploy a new version alongside the old one and switch traffic once validated.

  • Canary Deployments: Gradually shift traffic to a new version and monitor metrics.

  • Horizontal Pod Autoscaler (HPA v2): Scale based on CPU, memory, or custom metrics for adaptive performance.

These strategies improve uptime and reduce deployment risks.

Ensuring pod health & high availability#

Your deployment is only reliable if pods remain healthy and available.
Use Kubernetes’ built-in primitives to keep them that way:

  • Probes: Add liveness, readiness, and startup probes to detect failures early.

  • PodDisruptionBudget (PDB): Control how many pods can be taken down during maintenance.

  • unhealthyPodEvictionPolicy: Prevent healthy pods from being evicted before unhealthy ones.

  • Topology Spread Constraints: Distribute pods across zones and nodes to avoid single points of failure.

These features make your workloads more resilient and production-ready.

Choosing the right workload kind#

Deployment is the most common workload type, but it’s not the only option.
Choosing the right one ensures proper behavior:

  • StatefulSet: For apps needing stable network identities or persistent storage.

  • DaemonSet: For workloads that must run on every node (e.g., logging agents).

  • Job / CronJob: For one-time or scheduled batch jobs.

Use a simple decision matrix to guide selection based on your app’s needs.

Day-2 operations & debugging#

Running workloads is only half the story.
Once your apps are live, you’ll need to debug and optimize them.

  • Ephemeral containers: Attach a temporary debugging container to a running pod with kubectl debug.

  • Resource requests and limits: Prevent OOM kills and throttling by setting appropriate values.

  • QoS classes: Understand how Kubernetes prioritizes pods under resource pressure.

  • Diagnostics: Use kubectl describe, logs, and events to troubleshoot issues quickly.

These tools and practices help you manage real-world production clusters confidently.


Wrapping up#

greet got his parameters, curry stopped looping, and we’ve received our greeting: Hello, John Doe.

Play around with this function some more. Try supplying multiple or no parameters in one shot, get as crazy as you want. See how many times curry has to recurse before returning your expected output.

C++
curriedGreet('Hello', 'John', 'Doe');
curriedGreet('Hello', 'John')('Doe');
curriedGreet()()('Hello')()('John')()()()()('Doe');

Many thanks to Eric Elliott for introducing this to me, and even more thanks to you for appreciating curry with me. Until next time!

If you’d like more information on currying, you can visit Educative’s course Functional Programming Patterns with RamdaJS which gives you an in-depth look at currying, its advantages, and much more.

Happy learning!


Continue reading about Functional programming and JavaScript#


Written By:
Yazeed Bzadough