Skip to content

Function Composition

Function composition is the process of combining two or more functions to create a new function. The output of one function becomes the input of the next.

function compose<A, B, C>(
f: (b: B) => C,
g: (a: A) => B
): (a: A) => C {
return (a: A) => f(g(a));
}
// Example functions
const addOne = (n: number): number => n + 1;
const double = (n: number): number => n * 2;
const square = (n: number): number => n * n;
// Compose: reads right to left
const addOneThenDouble = compose(double, addOne);
addOneThenDouble(5); // double(addOne(5)) = double(6) = 12
const doubleThenSquare = compose(square, double);
doubleThenSquare(3); // square(double(3)) = square(6) = 36
function pipe<A, B, C>(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C {
return (a: A) => g(f(a));
}
// Pipe: reads left to right (more intuitive)
const squareThenDouble = pipe(square, double);
squareThenDouble(5); // double(square(5)) = double(25) = 50
const addOneThenSquare = pipe(addOne, square);
addOneThenSquare(5); // square(addOne(5)) = square(6) = 36
function composeMany<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}
// Usage
const transform = composeMany(
square,
double,
addOne
);
transform(5);
// addOne(5) = 6
// double(6) = 12
// square(12) = 144
function pipeMany<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}
// Usage
const process = pipeMany(
addOne, // 5 + 1 = 6
double, // 6 * 2 = 12
square // 12 * 12 = 144
);
process(5); // 144
interface User {
id: number;
name: string;
age: number;
active: boolean;
}
// Individual transformation functions
const getActiveUsers = (users: User[]): User[] =>
users.filter(user => user.active);
const sortByAge = (users: User[]): User[] =>
[...users].sort((a, b) => a.age - b.age);
const getUserNames = (users: User[]): string[] =>
users.map(user => user.name);
// Compose into pipeline
const getActiveUserNames = pipe(
getActiveUsers,
sortByAge,
getUserNames
);
const users: User[] = [
{ id: 1, name: 'John', age: 30, active: true },
{ id: 2, name: 'Jane', age: 25, active: false },
{ id: 3, name: 'Bob', age: 35, active: true }
];
getActiveUserNames(users);
// ['John', 'Bob']
const trim = (str: string): string => str.trim();
const lowercase = (str: string): string => str.toLowerCase();
const capitalize = (str: string): string =>
str.charAt(0).toUpperCase() + str.slice(1);
const normalizeString = pipe(
trim,
lowercase,
capitalize
);
normalizeString(' hello world ');
// 'Hello world'
const addTax = (rate: number) => (amount: number): number =>
amount * (1 + rate);
const applyDiscount = (discount: number) => (amount: number): number =>
amount * (1 - discount);
const formatCurrency = (amount: number): string =>
`$${amount.toFixed(2)}`;
// Calculate price with transformations
const calculateFinalPrice = pipe(
applyDiscount(0.1), // 10% discount
addTax(0.08), // 8% tax
formatCurrency
);
calculateFinalPrice(100);
// '$97.20'

Writing functions without explicitly mentioning arguments.

const numbers = [1, 2, 3, 4, 5];
const isEven = (x: number) => x % 2 === 0;
const double = (x: number) => x * 2;
// Explicit arguments
const result = numbers
.filter(x => isEven(x))
.map(x => double(x));
// No explicit arguments
const result = numbers
.filter(isEven)
.map(double);
// More point-free examples
const users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
];
// With points
const names1 = users.map(user => user.name);
// Point-free
const getName = (user: { name: string }) => user.name;
const names2 = users.map(getName);
// Even more point-free with property accessor
const prop = <K extends string>(key: K) =>
<T extends Record<K, any>>(obj: T): T[K] =>
obj[key];
const names3 = users.map(prop('name'));
function trace<T>(label: string) {
return (value: T): T => {
console.log(`${label}:`, value);
return value;
};
}
// Use in pipeline
const debugPipeline = pipe(
addOne,
trace('After addOne'),
double,
trace('After double'),
square,
trace('After square')
);
debugPipeline(5);
// After addOne: 6
// After double: 12
// After square: 144
function when<T>(
predicate: (value: T) => boolean,
fn: (value: T) => T
) {
return (value: T): T =>
predicate(value) ? fn(value) : value;
}
const processNumber = pipe(
when((n: number) => n < 0, Math.abs),
when((n: number) => n > 100, () => 100),
double
);
processNumber(-5); // 10
processNumber(150); // 200
processNumber(50); // 100
function tryCatch<T, E = Error>(
fn: (value: T) => T,
onError: (error: E, value: T) => T
) {
return (value: T): T => {
try {
return fn(value);
} catch (error) {
return onError(error as E, value);
}
};
}
const parseJSON = tryCatch(
(str: string) => JSON.parse(str),
(error, original) => {
console.error('JSON parse error:', error);
return original;
}
);
const processData = pipe(
trim,
parseJSON,
// ... more transformations
);
function pipeAsync<T>(...fns: Array<(arg: T) => Promise<T> | T>) {
return async (arg: T): Promise<T> => {
let result = arg;
for (const fn of fns) {
result = await fn(result);
}
return result;
};
}
// Async functions
const fetchUser = async (id: number) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const enrichUser = async (user: any) => {
const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json());
return { ...user, posts };
};
const processUser = pipeAsync(
fetchUser,
enrichUser,
user => ({ ...user, processed: true })
);
await processUser(1);

✅ Use pipe for left-to-right readability ✅ Keep composed functions pure ✅ Compose small, focused functions ✅ Use point-free style when clear ✅ Add trace functions for debugging ✅ Handle errors in composition ✅ Type your compositions properly ✅ Name composed functions descriptively

❌ Create overly complex compositions ❌ Compose impure functions ❌ Mix sync and async without handling ❌ Forget about error handling ❌ Over-use point-free (readability matters) ❌ Compose functions with mismatched types ❌ Create deeply nested compositions