Functional Programming in JavaScript
FP treats computation as evaluation of mathematical functions — avoiding shared mutable state and side effects.
Core principles:
- Pure functions — same input always produces same output, no side effects
- Immutability — don't mutate, create new values
- First-class functions — functions as values, passed and returned
- Composition — build complex behavior from simple functions
- Declarative — describe what, not how
Pure Functions
js// ❌ Impure — depends on external state, has side effects
let total = 0;
function addToTotal(n) {
total += n; // mutates external state
console.log(total); // side effect
}
// ✅ Pure — predictable, testable, no side effects
function add(a, b) { return a + b; }
function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}
// Pure function test — deterministic, no mocks needed
console.log(add(2, 3)); // always 5
console.log(formatCurrency(1234.5)); // always '$1,234.50'Immutability
Never mutate — return new values:
js// ❌ Mutation
const user = { name: 'Alice', age: 30 };
user.age = 31; // mutated original
// ✅ Immutable update
const updatedUser = { ...user, age: 31 }; // new object
// ❌ Array mutation
const arr = [1, 2, 3];
arr.push(4); // mutates
// ✅ Immutable array operations
const newArr = [...arr, 4]; // append
const without = arr.filter(x => x !== 2); // remove
const replaced = arr.map(x => x === 2 ? 99 : x); // replace
// Deep immutable update (nested)
const state = { user: { name: 'Alice', prefs: { theme: 'dark' } } };
const newState = {
...state,
user: {
...state.user,
prefs: { ...state.user.prefs, theme: 'light' }
}
};
// Structured Clone (deep copy for complex objects)
const deepCopy = structuredClone(state); // ES2022
// Object.freeze — shallow immutability
const config = Object.freeze({ apiUrl: 'https://api.example.com', timeout: 5000 });
config.apiUrl = 'changed'; // silently fails (throws in strict mode)
config.timeout; // 5000 — unchangedImmutable Data Structures (production)
js// Immer — write mutable-looking code that produces immutable result
import { produce } from 'immer';
const nextState = produce(state, (draft) => {
draft.user.prefs.theme = 'light'; // looks like mutation
draft.notifications.push({ id: 1, msg: 'Hello' });
}); // state unchanged, nextState is a new object
// Immer is used by Redux Toolkit internallyHigher-Order Functions
Functions that take or return other functions:
js// Built-in HOFs
[1, 2, 3].map(x => x * 2); // [2, 4, 6] — transform
[1, 2, 3, 4].filter(x => x % 2); // [1, 3] — select
[1, 2, 3].reduce((acc, x) => acc + x, 0); // 6 — accumulate
[1, 2, 3].every(x => x > 0); // true
[1, -1, 2].some(x => x < 0); // true
[3, 1, 2].sort((a, b) => a - b); // [1, 2, 3]
[1, [2, [3]]].flat(Infinity); // [1, 2, 3]
[1, 2].flatMap(x => [x, x * 2]); // [1, 2, 2, 4]
// Custom HOF
function withRetry(fn, maxAttempts = 3) {
return async function(...args) {
let lastError;
for (let i = 0; i < maxAttempts; i++) {
try {
return await fn(...args);
} catch (err) {
lastError = err;
if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, 2 ** i * 100));
}
}
throw lastError;
};
}
const fetchWithRetry = withRetry(fetch, 3);
// withLogger
function withLogger(fn, name = fn.name) {
return function(...args) {
console.time(name);
const result = fn.apply(this, args);
console.timeEnd(name);
return result;
};
}Currying
Currying transforms a function that takes N arguments into a chain of N single-argument functions. The key benefit is partial application via specialization: you call the curried function with some arguments up front and get back a new function pre-loaded with those arguments, ready to receive the rest later. This enables creating reusable, domain-specific function variants (double, triple, onClick) from a single general-purpose function without any wrapper boilerplate. Currying differs from partial application: currying always produces single-argument functions in a strict chain, while partial application fixes any number of arguments in one step. Use currying (or libraries like Ramda/fp-ts that curry by default) when building composable point-free pipelines.
js// Manual curry
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6
add(1, 2, 3); // 6
// Practical: create specialized functions
const multiply = curry((factor, n) => n * factor);
const double = multiply(2);
const triple = multiply(3);
[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]
// Event handling
const addEventListener = curry((event, handler, element) =>
element.addEventListener(event, handler)
);
const onClick = addEventListener('click');
const onButtonClick = onClick(() => console.log('clicked'));
// onButtonClick(buttonElement);Partial Application
Partial application fixes a subset of a function's arguments now and returns a new function that accepts the remaining arguments later. Unlike currying (which always produces single-argument chains), partial application can fix any number of arguments in one call. It solves the configuration problem: when you have a general-purpose function and a known context (a log level, a base URL, a multiplier), partial application creates a context-aware version without repeating the fixed argument at every call site. Function.prototype.bind is the built-in partial application mechanism.
jsfunction partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
function log(level, timestamp, message) {
console.log(`[${level}] ${timestamp}: ${message}`);
}
const logInfo = partial(log, 'INFO');
const logError = partial(log, 'ERROR');
logInfo(Date.now(), 'Server started');
logError(Date.now(), 'Connection failed');
// Using bind for partial application
const double = Math.pow.bind(null, 2); // base = 2
double(10); // 2^10 = 1024Function Composition
Combine multiple functions into one:
js// compose: right-to-left (mathematical order)
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
// pipe: left-to-right (data flow order — more readable)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
// Building a data transformation pipeline
const trim = s => s.trim();
const toLowerCase = s => s.toLowerCase();
const removeSpaces = s => s.replace(/\s+/g, '-');
const slugify = pipe(trim, toLowerCase, removeSpaces);
slugify(' Hello World '); // 'hello-world'
// Complex pipeline
const processUsers = pipe(
users => users.filter(u => u.active),
users => users.map(u => ({ ...u, name: u.name.trim() })),
users => users.sort((a, b) => a.name.localeCompare(b.name)),
users => users.slice(0, 10),
);
processUsers(rawUsers);Functors and Monads (simplified)
A functor is any container that implements a map method which applies a function to its contents and returns the same container type — Array is the most familiar example. A monad extends this with a flatMap/chain method that handles nested containers and sequencing. The practical value in JavaScript is handling nullable values and errors without null checks scattered throughout the code. The Maybe monad wraps a potentially-absent value; every .map() on a Nothing is a no-op, so you can chain transformations safely and only handle the absent case once at the end. Use this pattern when you find yourself writing deeply nested if (x && x.y && x.y.z) guards.
js// Functor: a container you can map over
// Array is a functor: [1,2,3].map(fn) → [fn(1), fn(2), fn(3)]
// Maybe monad — safe operations on nullable values
class Maybe {
#value;
constructor(value) { this.#value = value; }
static of(value) { return new Maybe(value); }
static empty() { return new Maybe(null); }
isNothing() { return this.#value == null; }
map(fn) {
return this.isNothing() ? Maybe.empty() : Maybe.of(fn(this.#value));
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.#value;
}
}
// No null checks scattered through code
const getCity = (user) =>
Maybe.of(user)
.map(u => u.address)
.map(a => a.city)
.getOrElse('Unknown');
getCity({ address: { city: 'NYC' } }); // 'NYC'
getCity({ address: null }); // 'Unknown'
getCity(null); // 'Unknown'Memoization
Cache results of expensive pure functions:
jsfunction memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn.apply(this, args));
return cache.get(key);
};
}
const fibonacci = memoize(function fib(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
fibonacci(40); // fast
fibonacci(40); // instant (cached)
// WeakMap-based memoize (for object args, avoids memory leaks)
function memoizeWeak(fn) {
const cache = new WeakMap();
return function(obj, ...rest) {
if (!cache.has(obj)) cache.set(obj, fn(obj, ...rest));
return cache.get(obj);
};
}Transducers (advanced)
Transducers are composable transformation functions that are independent of the data source and sink. Where chaining .filter().map() creates one intermediate array per step, a transducer composes the transformations into a single reducing function that processes each element exactly once. This makes transducers ideal for processing large arrays or streams where allocating intermediate collections is too expensive. Transducers are context-free: the same transducer can be applied to arrays, generators, observable streams, or any reducible container. The cost is a higher conceptual overhead compared to method chaining.
Composable, efficient data transformations — no intermediate arrays:
js// Regular: creates two intermediate arrays
const result = [1,2,3,4,5,6,7,8,9,10]
.filter(x => x % 2 === 0) // intermediate array
.map(x => x * 3); // intermediate array
// Transducer: single pass, no intermediates
const filterEven = reducer => (acc, x) => x % 2 === 0 ? reducer(acc, x) : acc;
const multiplyBy3 = reducer => (acc, x) => reducer(acc, x * 3);
const append = (acc, x) => [...acc, x];
const transduce = (...transducers) =>
(reducer) => transducers.reduceRight((r, t) => t(r), reducer);
const xform = transduce(filterEven, multiplyBy3);
[1,2,3,4,5,6,7,8,9,10].reduce(xform(append), []); // [6, 12, 18, 24, 30]Practical FP: Replacing Imperative Code
js// ❌ Imperative
function processOrders(orders) {
const result = [];
for (const order of orders) {
if (order.status === 'completed') {
const total = order.items.reduce((sum, item) => sum + item.price * item.qty, 0);
if (total > 100) {
result.push({ ...order, total, discount: total * 0.1 });
}
}
}
return result;
}
// ✅ Declarative FP
const calcTotal = order =>
order.items.reduce((sum, item) => sum + item.price * item.qty, 0);
const processOrders = pipe(
orders => orders.filter(o => o.status === 'completed'),
orders => orders.map(o => ({ ...o, total: calcTotal(o) })),
orders => orders.filter(o => o.total > 100),
orders => orders.map(o => ({ ...o, discount: o.total * 0.1 })),
);