Function Composition
What is Function Composition?
Section titled “What is 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.
Basic Composition
Section titled “Basic Composition”Compose (Right to Left)
Section titled “Compose (Right to Left)”function compose<A, B, C>( f: (b: B) => C, g: (a: A) => B): (a: A) => C { return (a: A) => f(g(a));}
// Example functionsconst addOne = (n: number): number => n + 1;const double = (n: number): number => n * 2;const square = (n: number): number => n * n;
// Compose: reads right to leftconst addOneThenDouble = compose(double, addOne);addOneThenDouble(5); // double(addOne(5)) = double(6) = 12
const doubleThenSquare = compose(square, double);doubleThenSquare(3); // square(double(3)) = square(6) = 36Pipe (Left to Right)
Section titled “Pipe (Left to Right)”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) = 36Multi-Function Composition
Section titled “Multi-Function Composition”Compose Many Functions
Section titled “Compose Many Functions”function composeMany<T>(...fns: Array<(arg: T) => T>): (arg: T) => T { return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);}
// Usageconst transform = composeMany( square, double, addOne);
transform(5);// addOne(5) = 6// double(6) = 12// square(12) = 144Pipe Many Functions
Section titled “Pipe Many Functions”function pipeMany<T>(...fns: Array<(arg: T) => T>): (arg: T) => T { return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);}
// Usageconst process = pipeMany( addOne, // 5 + 1 = 6 double, // 6 * 2 = 12 square // 12 * 12 = 144);
process(5); // 144Real-World Examples
Section titled “Real-World Examples”Data Transformation Pipeline
Section titled “Data Transformation Pipeline”interface User { id: number; name: string; age: number; active: boolean;}
// Individual transformation functionsconst 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 pipelineconst 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']String Processing
Section titled “String Processing”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'Number Calculations
Section titled “Number Calculations”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 transformationsconst calculateFinalPrice = pipe( applyDiscount(0.1), // 10% discount addTax(0.08), // 8% tax formatCurrency);
calculateFinalPrice(100);// '$97.20'Point-Free Style
Section titled “Point-Free Style”Writing functions without explicitly mentioning arguments.
With Points (Arguments)
Section titled “With Points (Arguments)”const numbers = [1, 2, 3, 4, 5];
const isEven = (x: number) => x % 2 === 0;const double = (x: number) => x * 2;
// Explicit argumentsconst result = numbers .filter(x => isEven(x)) .map(x => double(x));Point-Free Style
Section titled “Point-Free Style”// No explicit argumentsconst result = numbers .filter(isEven) .map(double);Benefits
Section titled “Benefits”// More point-free examplesconst users = [ { name: 'John', age: 30 }, { name: 'Jane', age: 25 }];
// With pointsconst names1 = users.map(user => user.name);
// Point-freeconst getName = (user: { name: string }) => user.name;const names2 = users.map(getName);
// Even more point-free with property accessorconst prop = <K extends string>(key: K) => <T extends Record<K, any>>(obj: T): T[K] => obj[key];
const names3 = users.map(prop('name'));Composition Patterns
Section titled “Composition Patterns”Trace for Debugging
Section titled “Trace for Debugging”function trace<T>(label: string) { return (value: T): T => { console.log(`${label}:`, value); return value; };}
// Use in pipelineconst debugPipeline = pipe( addOne, trace('After addOne'), double, trace('After double'), square, trace('After square'));
debugPipeline(5);// After addOne: 6// After double: 12// After square: 144Conditional Composition
Section titled “Conditional Composition”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); // 10processNumber(150); // 200processNumber(50); // 100Try-Catch Composition
Section titled “Try-Catch Composition”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);Advanced Composition
Section titled “Advanced Composition”Async Composition
Section titled “Async Composition”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 functionsconst 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);Best Practices
Section titled “Best Practices”✅ 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
Don’ts
Section titled “Don’ts”❌ 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