mdashikjs/blog
All posts
Currying and Partial Application: Boring Name, Useful Tool
Frontend

Currying and Partial Application: Boring Name, Useful Tool

Frontend4 min

Currying and Partial Application: Boring Name, Useful Tool

I used to roll my eyes at currying. It felt like FP showing off. Then I started using it for configured loggers, middleware, and event handlers, and it quietly became part of my toolkit.

JavaScriptFunctional ProgrammingTypeScript
Share:

The two terms, plainly

Partial application: you pre-fill some arguments of a function and get back a function that takes the rest.

Currying: you turn f(a, b, c) into f(a)(b)(c). Every call takes exactly one argument.

In practice I rarely reach for full currying. Partial application is what I use daily.

Configured loggers

function log(level, context, message) {
  console.log(`[${level}] [${context}] ${message}`);
}

const authLog = log.bind(null, 'INFO', 'auth');

authLog('user logged in');
authLog('token refreshed');

bind is partial application built into the language. Pass null as thisArg and pre-fill arguments. No library needed.

Handler factories

In React and DOM code I use partial application to avoid rebuilding handlers inline:

const remove = (id) => () => setItems((prev) => prev.filter((x) => x.id !== id));

<button onClick={remove(item.id)}>Remove</button>

remove(item.id) returns a function closed over id. Each button gets its own handler, no onClick={() => remove(item.id)} wrapper. Slightly cleaner, identical behavior.

Express/Koa middleware

Middleware factories are partial application in disguise:

function requireRole(role) {
  return (req, res, next) => {
    if (req.user?.role !== role) return res.sendStatus(403);
    next();
  };
}

app.get('/admin', requireRole('admin'), adminHandler);

You partially apply role, get back a middleware that closes over it. This is the default pattern for configurable middleware in the Node ecosystem.

When currying specifically helps

True currying shines when you want to build a pipeline and every step takes one argument:

import { curry } from 'lodash';

const filter = curry((pred, arr) => arr.filter(pred));
const map = curry((fn, arr) => arr.map(fn));

const activeNames = pipe(
  filter((u) => u.active),
  map((u) => u.name),
);

activeNames(users);

The data flows through at the end. Each step is reusable. This is the functional style that libraries like Ramda optimize for.

The TypeScript consideration

Currying plays poorly with overloads and default generics. A function like (a: number, b: string) => boolean has a clean type. Its curried form (a: number) => (b: string) => boolean is fine too, but stacked generics across layers can trip up inference. I curry rarely and explicitly in TypeScript, and reach for partial application via closures when I want flexibility.

The payoff

Partial application turns "a function that needs config and data" into two calls — configure once, use many times. That is the whole of the idea, and it quietly cleans up a lot of otherwise repetitive code.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.