Higher-Order Functions
Functions that accept other functions as arguments or return them — enabling composable, reusable abstractions without classes or inheritance.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
A higher-order function (HOF) is a function that does at least one of: takes one or more functions as arguments; returns a function as its result. The concept comes from mathematics (functors, combinators) and entered programming with Lisp (1958).
The canonical HOFs are map, filter, and reduce — they abstract over looping and let you express transformations declaratively. Instead of a for loop that iterates, checks a condition, and accumulates a result, you compose map(transform).filter(predicate).reduce(combine).
HOFs also enable partial application and currying: a function that takes some arguments and returns a new function waiting for the rest. This produces reusable, composable building blocks — a logger that pre-captures the context, a validator that pre-captures the rules, a fetch that pre-captures the base URL.
In TypeScript, Go, and Rust, functions are first-class values — they can be stored in variables, passed as arguments, and returned. This is all HOFs require. No classes needed.
Implementation
TypeScript · Go · Rustconst double = (n: number) => n * 2;
const isEven = (n: number) => n % 2 === 0;
const toString = (n: number) => `${n}`;
// map / filter / reduce — canonical higher-order functions
const result = [1, 2, 3, 4, 5]
.filter(isEven) // [2, 4]
.map(double) // [4, 8]
.map(toString); // ["4", "8"]
// HOF that returns a function — adds behaviour without changing the original
function withLogging<T extends unknown[], R>(
name: string,
fn: (...args: T) => R
): (...args: T) => R {
return (...args) => {
console.log(`[${name}] args:`, args);
const result = fn(...args);
console.log(`[${name}] result:`, result);
return result;
};
}
const loggedDouble = withLogging("double", double);
loggedDouble(5); // logs input and output, returns 10
// Partial application — pre-fill some arguments
const multiply = (a: number) => (b: number) => a * b;
const triple = multiply(3);
[1, 2, 3].map(triple); // [3, 6, 9]Why it matters
HOFs eliminate repetition at the logic level, not just the data level. Instead of copying a loop pattern three times with different bodies, you write one function and pass the varying behaviour as a parameter. The result is more expressive, shorter, and easier to test.
✓ When to use
- →Data transformations (map, filter, reduce) over collections
- →Middleware pipelines (HTTP, Redux, Express) — each step is a function
- →Decorator / wrapping pattern — add logging, caching, retry by wrapping a function
- →Partial application to create specialised versions of a general function
✗ When NOT to use
- →When the function being passed is very complex — a named class method may be more readable
- →Deep nesting of HOFs becomes harder to debug than an equivalent imperative loop
Trade-offs
Eliminate boilerplate loops — express intent, not mechanics
Deeply chained HOFs can be hard to debug (no named variables at each step)
Composable building blocks — combine small functions into complex pipelines
Performance cost of creating many closure objects in tight loops
Functions become data — storable, passable, configurable
Stack traces through HOFs and closures are harder to read
In production
Higher-order components (HOC) — wrap a component and return an enhanced component. Custom hooks are HOFs that return reactive values
Middleware is a HOF — each layer takes (req, res, next) and returns nothing, but wraps the next layer
Operators (map, filter, mergeMap, debounceTime) are HOFs over Observables — compose entire async pipelines from small functions
Industry adoption
Related principles
Pure Functions
A pure function always produces the same output for the same input and causes no side effects — making it predictable, testable, and safe to run anywhere.
Immutability
Never modify existing data — create a new value instead. Immutable data is safe to share, easy to reason about, and eliminates a whole class of mutation bugs.
Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.
Observer Pattern
Let a subject notify a dynamic list of dependents automatically when its state changes — without the subject knowing who is listening.