The ‘Wet Yarn’ Problem
I once spent three full days debugging a single 2,000-line file where a global variable was being mutated by five different functions. It was like trying to untangle a box of old Christmas lights in the dark. Every time I fixed a bug in one corner, two more popped up elsewhere. That was the moment I realized that imperative programming—while great for quick scripts—often collapses under the weight of a large-scale application.
Functional Programming (FP) isn’t some academic theory reserved for Haskell enthusiasts. It is a practical toolkit for managing data flow. Instead of micromanaging the computer’s state step-by-step, you describe how data should transform. In my experience, moving toward this mindset is the single most effective way to build systems that don’t break every time you touch them.
By adopting Currying, Pipe, and Composition, you swap “Spaghetti Code” for a “Lego Block” architecture. Each function becomes a small, predictable, and independent unit. Let’s look at how to implement these patterns in JavaScript and TypeScript to save your sanity.
Setting Up for Success
You don’t need a massive library to start using FP. However, I highly recommend TypeScript. Its type system acts as a safety rail, ensuring that the output of function A actually fits into the input of function B. This prevents those annoying undefined is not a function errors before they ever reach production.
To get a playground running, initialize a clean TypeScript project in your terminal:
# Create a project folder
mkdir fp-lab && cd fp-lab
# Quick npm setup
npm init -y
# Install the essentials
npm install typescript ts-node @types/node --save-dev
# Generate config
npx tsc --init
Create an index.ts file and use ts-node to run your code. This lightweight setup is perfect for sketching out logic without the overhead of a full build pipeline.
Core Utilities
We’ll build our core tools from scratch. Understanding the “why” behind these patterns makes them much easier to use in a real project.
1. Currying: Pre-filling Your Logic
Currying turns a function with many arguments into a series of functions that take one argument at a time. It’s perfect for creating specialized tools out of generic ones. Imagine you’re building a notification system. You don’t want to pass the severity level every single time you log a message.
// The generic version
const log = (level: string, message: string) => {
console.log(`[${new Date().toISOString()}] [${level}] ${message}`);
};
// The curried version
const curriedLog = (level: string) => (message: string) => {
console.log(`[${level}] ${message}`);
};
const criticalError = curriedLog("CRITICAL");
const userNotice = curriedLog("NOTICE");
criticalError("DB Connection lost!"); // [CRITICAL] DB Connection lost!
userNotice("User logged in."); // [NOTICE] User logged in.
2. Function Composition: Building the Chain
Composition is the math-heavy sibling of FP. It combines multiple functions so that compose(f, g)(x) works like f(g(x)). The data flows from right to left, which is great for mathematical purity.
const double = (n: number) => n * 2;
const plusTen = (n: number) => n + 10;
// Right-to-left flow
const compose = <T>(...fns: Array<(arg: T) => T>) =>
(value: T) => fns.reduceRight((acc, fn) => fn(acc), value);
const processValue = compose(double, plusTen);
console.log(processValue(5)); // (5 + 10) * 2 = 30
3. Thinking in Pipelines (Pipe)
Most developers prefer pipe over compose because it reads left-to-right, just like English. It’s intuitive. You take a piece of data and pass it through a series of stations, each modifying it slightly before handing it off to the next.
const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T) => fns.reduce((acc, fn) => fn(acc), value);
// String cleanup pipeline
const trim = (s: string) => s.trim();
const shout = (s: string) => s.toUpperCase();
const tag = (s: string) => `[LOG]: ${s}`;
const prepareHeader = pipe(trim, shout, tag);
console.log(prepareHeader(" welcome home "));
// "[LOG]: WELCOME HOME"
In production, I use pipe to handle complex data transformations in Redux reducers or Express middleware. It keeps the logic flat. You avoid the “Pyramid of Doom” where you have five closing parentheses at the end of a single line.
Testing and Observability
FP makes testing almost boring. Since pure functions don’t touch global state, you don’t need complex mocks or beforeEach resets. Every test is a simple input-output check.
import { describe, it, expect } from 'vitest';
describe('FP Pipeline', () => {
it('should transform strings predictably', () => {
const output = prepareHeader(" test ");
expect(output).toBe("[LOG]: TEST");
});
});
Debugging the Pipeline
A common gripe with pipe is that you can’t easily set a breakpoint inside the chain. To fix this, I use a trace helper. It logs the value and passes it through without changing it.
const trace = <T>(label: string) => (value: T) => {
console.log(`${label}:`, value);
return value;
};
const debugPipeline = pipe(
trim,
trace("Post-Trim"),
shout,
trace("Post-Shout"),
tag
);
This approach turns debugging into a systematic observation of data evolution. You stop hunting for rogue mutations hidden in a 500-line class. Instead, you watch exactly how your data grows and changes at every step. Start by refactoring one small utility file—the clarity in your next code review will be worth the effort.

