In our last post, we built a custom pipe function from scratch. It solved a massive readability problem by turning deeply nested function calls into a beautiful, linear top-to-bottom pipeline:
// The goal we achieved last month:
const result = pipe(
initialValue,
doubleScore,
addBonusPoint
);But as soon as you tried to use this custom pipe in your real-world code, you likely hit a massive roadblock.
pipe relies on a strict rule: every function in the chain must be unary, meaning it accepts exactlyone argument. The data flows out of function A and straight into function B as its sole input.
What happens when you need to pass a function that requires two or three arguments? For example, a function that adjusts a score by a variable difficulty multiplier?
const applyMultiplier = (multiplier: number, score: number): number => score * multiplier;
// ❌ TypeScript Error: pipe expects unary functions!
const result = pipe(
initialValue,
applyMultiplier // Wait, how do we pass the multiplier here?
);Today, we are going to fix this. We'll explore two closely related functional techniques—Partial Application and Currying—to transform multi-argument functions into perfectly shaped components for our pipelines.
Partial application is the process of taking a function with multiple arguments, fixing (or "pre-loading") some of those arguments up front, and returning a new function that takes the remaining arguments later.
In modern TypeScript, the easiest way to partially apply a function is by wrapping it in an arrow function.
const applyMultiplier = (multiplier: number, score: number): number => score * multiplier;
// We "pre-load" the multiplier argument with a value of 2
const doubleScore = (score: number) => applyMultiplier(2, score);
// Now it fits perfectly in our pipe!
const result = pipe(
initialValue,
doubleScore // Takes 1 argument, works smoothly
);This is incredibly readable and requires zero helper libraries. If you only have one or two instances where arguments don't align, inline partial application using standard arrow functions is usually your best bet.
While manual partial application is great, functional programming introduces a more systematic design pattern: Currying.
Currying is the technique of converting a function that takes multiple arguments into a sequence of functions, each taking a single argument.
Instead of writing a function like this:
const add = (a: number, b: number) => a + b;A curried version looks like this:
const curriedAdd = (a: number) => (b: number) => a + b;Let's rewrite our applyMultiplier function using the curried approach. Pay close attention to the argument order: we put the configuration data first, and the primary data (score) last.
// A curried function: configuration first, data last
const applyMultiplierCurried = (multiplier: number) => (score: number): number => {
return score * multiplier;
};
// Calling it step-by-step:
const triple = applyMultiplierCurried(3); // Returns a function: (score: number) => number
const finalScore = triple(100); // Returns 300Because invoking applyMultiplierCurried(3) yields a single-argument function waiting for a score, we can drop it directly into our pipeline without any wrapper functions:
const result = pipe(
initialValue, // Invoke to get initial value
applyMultiplierCurried(1.5), // Returns a unary function that pipe executes
addBonusPoint
);When writing curried functions for functional pipelines, the order of your parameters dictates how usable your function is. Always follow the Data-Last Rule:

From nested functions to clean pipelines: how partial application and currying transform multi-argument functions into reusable pieces.
Look at how JavaScript's native array methods get this backwards for pipelines, and how currying fixes it:
// Standard array method: data (this) + callback argument
// Functional approach: callback first, data last (better for composition)
const map = <T, U>(callback: (item: T) => U) => (array: T[]): U[] => {
return array.map(callback);
};
const filter = <T>(predicate: (item: T) => boolean) => (array: T[]): T[] => {
return array.filter(predicate);
};
// Now we can construct clean pipelines for data processing:
const usersWithEmails = pipe(
fetchUsers(),
filter(user => user.isActive), // Pre-loaded with predicate
map(user => user.email.toLowerCase()) // Pre-loaded with transformer
);curry UtilityWhat if you want to curry an existing function from a third-party library without rewriting it from scratch? We can write a lightweight TypeScript helper to automate this.
For a basic two-argument function, the type signature and implementation look like this. The name curry2 indicates it curries a function with 2 arguments:
// curry2 works with functions that take 2 arguments
function curry2<A, B, R>(fn: (a: A, b: B) => R) {
return (a: A) => (b: B): R => fn(a, b);
}
// Example usage with EUR instead of hardcoded currency:
const legacyFormat = (currency: string, amount: number) => `${currency}${amount}`;
const formatEUR = curry2(legacyFormat)('€');
const displayPrices = pipe(
[10, 25, 50],
map(formatEUR) // ['€10', '€25', '€50']
);By shifting your mindset to design functions with the Data-Last rule, your TypeScript codebase will quickly assemble into a clean puzzle where every piece effortlessly locks together.