JavaScript — Higher-Order Functions
A higher-order function (HOF) is a function that does at least one of the following: takes one or more functions as arguments, returns a function as its result, or both. JavaScript treats functions as first-class citizens, which makes HOFs a natural and powerful part of the language. They are the cornerstone of functional programming in JavaScript.
The built-in array methods — map, filter, reduce, find, some, every, sort — are the most commonly used HOFs. Beyond these, HOFs enable advanced patterns like function composition, currying, memoization, and decorators. These patterns allow you to write more declarative, reusable, and composable code.
Higher-order functions allow you to abstract over actions, not just values. Instead of writing explicit loops and conditionals, you can express what you want to accomplish and let the HOF handle the mechanics. This leads to code that is shorter, more readable, and less prone to off-by-one errors.
| 1 | // HOF: takes a function as argument |
| 2 | function withLogging(fn) { |
| 3 | return function(...args) { |
| 4 | console.log(`→ ${fn.name || "anonymous"}(${args})`); |
| 5 | const result = fn(...args); |
| 6 | console.log(`← ${result}`); |
| 7 | return result; |
| 8 | }; |
| 9 | } |
| 10 | |
| 11 | const add = (a, b) => a + b; |
| 12 | const loggedAdd = withLogging(add); |
| 13 | loggedAdd(3, 4); |
| 14 | // → add(3,4) |
| 15 | // ← 7 |
| 16 | |
| 17 | // HOF: returns a function |
| 18 | const multiplyBy = (factor) => (x) => x * factor; |
| 19 | const double = multiplyBy(2); |
| 20 | const triple = multiplyBy(3); |
The defining characteristic of a higher-order function is that it operates on functions. This can mean accepting a callback, returning a new function, or both. This ability to manipulate functions as data enables powerful abstraction patterns.
Taking Functions (Callbacks)
| 1 | // HOF that takes a function: array methods |
| 2 | const numbers = [1, 2, 3, 4, 5]; |
| 3 | |
| 4 | // map — transform each element |
| 5 | const doubled = numbers.map(n => n * 2); |
| 6 | console.log(doubled); // [2, 4, 6, 8, 10] |
| 7 | |
| 8 | // filter — keep matching elements |
| 9 | const evens = numbers.filter(n => n % 2 === 0); |
| 10 | console.log(evens); // [2, 4] |
| 11 | |
| 12 | // Custom HOF: timing wrapper |
| 13 | function timed(fn) { |
| 14 | return function(...args) { |
| 15 | const start = performance.now(); |
| 16 | const result = fn(...args); |
| 17 | const elapsed = performance.now() - start; |
| 18 | console.log(`${fn.name} took ${elapsed.toFixed(2)}ms`); |
| 19 | return result; |
| 20 | }; |
| 21 | } |
| 22 | |
| 23 | const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2); |
| 24 | const timedFib = timed(fib); |
| 25 | console.log(timedFib(40)); // logs timing info |
| 26 | |
| 27 | // Custom HOF: retry wrapper |
| 28 | function withRetry(fn, maxRetries = 3) { |
| 29 | return async function(...args) { |
| 30 | let lastError; |
| 31 | for (let attempt = 1; attempt <= maxRetries; attempt++) { |
| 32 | try { |
| 33 | return await fn(...args); |
| 34 | } catch (error) { |
| 35 | lastError = error; |
| 36 | console.warn(`Attempt ${attempt}/${maxRetries} failed`); |
| 37 | if (attempt < maxRetries) { |
| 38 | await new Promise(r => setTimeout(r, 1000 * attempt)); |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | throw lastError; |
| 43 | }; |
| 44 | } |
Returning Functions (Factories)
| 1 | // HOF that returns a function: factory pattern |
| 2 | function createValidator(rules) { |
| 3 | return function(data) { |
| 4 | const errors = []; |
| 5 | for (const { field, validate, message } of rules) { |
| 6 | if (data[field] !== undefined && !validate(data[field])) { |
| 7 | errors.push({ field, message }); |
| 8 | } |
| 9 | } |
| 10 | return { valid: errors.length === 0, errors }; |
| 11 | }; |
| 12 | } |
| 13 | |
| 14 | const userValidator = createValidator([ |
| 15 | { field: "name", validate: (v) => v && v.length >= 2, message: "Name too short" }, |
| 16 | { field: "email", validate: (v) => /^.+@.+$/.test(v), message: "Invalid email" }, |
| 17 | { field: "age", validate: (v) => v >= 18, message: "Must be 18+" }, |
| 18 | ]); |
| 19 | |
| 20 | console.log(userValidator({ name: "A", email: "bad", age: 15 })); |
| 21 | // { valid: false, errors: [...] } |
| 22 | |
| 23 | // HOF that both takes and returns: middleware pattern |
| 24 | function composeMiddleware(...middlewares) { |
| 25 | return function(initialValue) { |
| 26 | return middlewares.reduce((value, middleware) => { |
| 27 | return middleware(value); |
| 28 | }, initialValue); |
| 29 | }; |
| 30 | } |
| 31 | |
| 32 | const addTimestamp = (data) => ({ ...data, timestamp: Date.now() }); |
| 33 | const addId = (data) => ({ ...data, id: crypto.randomUUID() }); |
| 34 | const sanitize = (data) => { |
| 35 | const { password, ...safe } = data; |
| 36 | return safe; |
| 37 | }; |
| 38 | |
| 39 | const processUser = composeMiddleware(addTimestamp, addId, sanitize); |
| 40 | const user = processUser({ name: "Alice", password: "secret" }); |
| 41 | console.log(user); |
| 42 | // { name: "Alice", timestamp: 1712345678, id: "uuid-..." } |
info
JavaScript arrays come with a rich set of built-in HOFs that cover most iteration needs. These methods replace explicit for loops with declarative operations, making code more readable and less error-prone. Understanding when to use each method is key to writing idiomatic JavaScript.
| Method | Returns | Use Case | Mutates Original? |
|---|---|---|---|
| map | New array (same length) | Transform every element | No |
| filter | New array (subset) | Select elements by condition | No |
| reduce | Single accumulated value | Aggregate, compute, group | No |
| find | First match (or undefined) | Find single element by condition | No |
| some | boolean | Check if any match | No |
| every | boolean | Check if all match | No |
| sort | Same array (sorted) | Order elements | Yes (⚠) |
| forEach | undefined | Side effects per element | No (but can via side effects) |
| flatMap | New array (flattened) | Map + flatten one level | No |
| 1 | const users = [ |
| 2 | { id: 1, name: "Alice", age: 30, active: true, role: "admin" }, |
| 3 | { id: 2, name: "Bob", age: 17, active: true, role: "user" }, |
| 4 | { id: 3, name: "Charlie", age: 25, active: false, role: "user" }, |
| 5 | { id: 4, name: "Diana", age: 35, active: true, role: "admin" }, |
| 6 | { id: 5, name: "Eve", age: 22, active: true, role: "user" }, |
| 7 | ]; |
| 8 | |
| 9 | // map — transform |
| 10 | const names = users.map(u => u.name); |
| 11 | const summaries = users.map(u => `${u.name} (${u.role})`); |
| 12 | |
| 13 | // filter — subset |
| 14 | const activeUsers = users.filter(u => u.active); |
| 15 | const adults = users.filter(u => u.age >= 18); |
| 16 | const admins = users.filter(u => u.role === "admin"); |
| 17 | |
| 18 | // reduce — aggregate |
| 19 | const totalAge = users.reduce((sum, u) => sum + u.age, 0); |
| 20 | const averageAge = totalAge / users.length; |
| 21 | |
| 22 | const groupedByRole = users.reduce((groups, u) => { |
| 23 | (groups[u.role] = groups[u.role] || []).push(u); |
| 24 | return groups; |
| 25 | }, {}); |
| 26 | |
| 27 | const oldestUser = users.reduce((oldest, u) => |
| 28 | u.age > oldest.age ? u : oldest |
| 29 | ); |
| 30 | |
| 31 | // find — first match |
| 32 | const alice = users.find(u => u.name === "Alice"); |
| 33 | const firstAdmin = users.find(u => u.role === "admin"); |
| 34 | |
| 35 | // some — any match |
| 36 | const hasMinors = users.some(u => u.age < 18); |
| 37 | const hasAnyInactive = users.some(u => !u.active); |
| 38 | |
| 39 | // every — all match |
| 40 | const allNamed = users.every(u => u.name && u.name.length > 0); |
| 41 | const allActive = users.every(u => u.active); // false |
| 42 | |
| 43 | // sort — with compare function (creates copy first) |
| 44 | const byAge = [...users].sort((a, b) => a.age - b.age); |
| 45 | const byName = [...users].sort((a, b) => a.name.localeCompare(b.name)); |
| 46 | |
| 47 | // flatMap — map and flatten |
| 48 | const tags = ["js,es6", "react,hooks"]; |
| 49 | const allTags = tags.flatMap(s => s.split(",")); |
| 50 | // ["js", "es6", "react", "hooks"] |
Advanced reduce Patterns
| 1 | // reduce is the most versatile array HOF |
| 2 | // Anything you can do with map/filter/find, you can do with reduce |
| 3 | |
| 4 | // Map with reduce |
| 5 | const doubled = numbers.reduce((acc, n) => [...acc, n * 2], []); |
| 6 | |
| 7 | // Filter with reduce |
| 8 | const evens = numbers.reduce((acc, n) => n % 2 === 0 ? [...acc, n] : acc, []); |
| 9 | |
| 10 | // FlatMap with reduce |
| 11 | const flatMapped = arr.reduce((acc, item) => [...acc, ...fn(item)], []); |
| 12 | |
| 13 | // Chaining with reduce (pipeline) |
| 14 | const pipeline = [n => n * 2, n => n + 1, n => `Result: ${n}`]; |
| 15 | const result = pipeline.reduce((value, fn) => fn(value), 5); |
| 16 | console.log(result); // "Result: 11" |
| 17 | |
| 18 | // Run promises in sequence |
| 19 | async function sequence(tasks) { |
| 20 | return tasks.reduce((promise, task) => |
| 21 | promise.then(results => task().then(r => [...results, r])), |
| 22 | Promise.resolve([]) |
| 23 | ); |
| 24 | } |
| 25 | |
| 26 | // Group by, count by, min/max |
| 27 | const maxBy = (arr, fn) => |
| 28 | arr.reduce((max, item) => fn(item) > fn(max) ? item : max); |
| 29 | |
| 30 | const minBy = (arr, fn) => |
| 31 | arr.reduce((min, item) => fn(item) < fn(min) ? item : min); |
| 32 | |
| 33 | const countBy = (arr, fn) => |
| 34 | arr.reduce((counts, item) => { |
| 35 | const key = fn(item); |
| 36 | counts[key] = (counts[key] || 0) + 1; |
| 37 | return counts; |
| 38 | }, {}); |
| 39 | |
| 40 | console.log(countBy(users, u => u.role)); // { admin: 2, user: 3 } |
pro tip
Function composition is the process of combining two or more functions to produce a new function. The output of one function becomes the input of the next. Composition is a fundamental concept in functional programming — it allows building complex operations from simple, focused functions, creating data transformation pipelines.
| 1 | // Compose (right-to-left) |
| 2 | function compose(...fns) { |
| 3 | return (x) => fns.reduceRight((acc, fn) => fn(acc), x); |
| 4 | } |
| 5 | |
| 6 | // Pipe (left-to-right) — more intuitive for data flow |
| 7 | function pipe(...fns) { |
| 8 | return (x) => fns.reduce((acc, fn) => fn(acc), x); |
| 9 | } |
| 10 | |
| 11 | // Example: data transformation pipeline |
| 12 | const users = [ |
| 13 | { name: "Alice", age: 30, active: true }, |
| 14 | { name: "Bob", age: 17, active: true }, |
| 15 | { name: "Charlie", age: 25, active: false }, |
| 16 | { name: "Diana", age: 35, active: true } |
| 17 | ]; |
| 18 | |
| 19 | // Building blocks — small, focused, reusable |
| 20 | const filterActive = (arr) => arr.filter(u => u.active); |
| 21 | const filterAdults = (arr) => arr.filter(u => u.age >= 18); |
| 22 | const pluckName = (arr) => arr.map(u => u.name); |
| 23 | const toUpper = (arr) => arr.map(n => n.toUpperCase()); |
| 24 | const sortAlpha = (arr) => [...arr].sort(); |
| 25 | |
| 26 | // Compose them into a pipeline |
| 27 | const getActiveAdultNames = pipe( |
| 28 | filterActive, |
| 29 | filterAdults, |
| 30 | pluckName, |
| 31 | toUpper, |
| 32 | sortAlpha |
| 33 | ); |
| 34 | |
| 35 | console.log(getActiveAdultNames(users)); // ["ALICE", "DIANA"] |
| 36 | |
| 37 | // Composable validation |
| 38 | const required = (msg) => (v) => v ? null : msg; |
| 39 | const minLength = (len) => (v) => v.length >= len ? null : `Min ${len} chars`; |
| 40 | const matches = (pattern) => (v) => pattern.test(v) ? null : "Invalid format"; |
| 41 | const composeValidators = (...validators) => (value) => |
| 42 | validators.reduce((errors, validate) => { |
| 43 | const error = validate(value); |
| 44 | return error ? [...errors, error] : errors; |
| 45 | }, []); |
| 46 | |
| 47 | const validatePassword = composeValidators( |
| 48 | required("Password is required"), |
| 49 | minLength(8), |
| 50 | matches(/[A-Z]/), |
| 51 | matches(/[0-9]/) |
| 52 | ); |
| 53 | |
| 54 | console.log(validatePassword("weak")); // ["Min 8 chars", "Must have uppercase", ...] |
| 55 | console.log(validatePassword("Strong1")); // [] — valid! |
info
Currying transforms a function that takes multiple arguments into a chain of functions each taking a single argument. Partial application pre-fills some arguments of a function, returning a function that accepts the remaining arguments. Both leverage closures and are essential for building composable, reusable function pipelines.
| 1 | // CURRYING — each argument becomes a nested function |
| 2 | const add = a => b => c => a + b + c; |
| 3 | console.log(add(1)(2)(3)); // 6 |
| 4 | |
| 5 | // Generic curry function |
| 6 | function curry(fn, arity = fn.length) { |
| 7 | return function curried(...args) { |
| 8 | if (args.length >= arity) { |
| 9 | return fn(...args); |
| 10 | } |
| 11 | return (...more) => curried(...args, ...more); |
| 12 | }; |
| 13 | } |
| 14 | |
| 15 | const curriedSum = curry((a, b, c) => a + b + c); |
| 16 | console.log(curriedSum(1, 2, 3)); // 6 |
| 17 | console.log(curriedSum(1)(2, 3)); // 6 |
| 18 | console.log(curriedSum(1)(2)(3)); // 6 |
| 19 | |
| 20 | // PARTIAL APPLICATION — fix some arguments |
| 21 | function partial(fn, ...presetArgs) { |
| 22 | return function(...laterArgs) { |
| 23 | return fn(...presetArgs, ...laterArgs); |
| 24 | }; |
| 25 | } |
| 26 | |
| 27 | const greet = (greeting, name) => `${greeting}, ${name}!`; |
| 28 | const sayHello = partial(greet, "Hello"); |
| 29 | const sayGoodbye = partial(greet, "Goodbye"); |
| 30 | |
| 31 | console.log(sayHello("Alice")); // "Hello, Alice!" |
| 32 | console.log(sayGoodbye("Bob")); // "Goodbye, Bob!" |
| 33 | |
| 34 | // Practical: curried map/filter for composability |
| 35 | const map = (fn) => (arr) => arr.map(fn); |
| 36 | const filter = (pred) => (arr) => arr.filter(pred); |
| 37 | const reduce = (fn, init) => (arr) => arr.reduce(fn, init); |
| 38 | |
| 39 | const double = map(x => x * 2); |
| 40 | const isEven = filter(x => x % 2 === 0); |
| 41 | const sum = reduce((a, b) => a + b, 0); |
| 42 | |
| 43 | const sumEvensDoubled = pipe(isEven, double, sum); |
| 44 | console.log(sumEvensDoubled([1, 2, 3, 4, 5])); // (2+4)*2 = 12 |
| 45 | |
| 46 | // Curried fetch with partial application |
| 47 | const apiRequest = (method) => (url) => (data) => |
| 48 | fetch(url, { |
| 49 | method, |
| 50 | headers: { "Content-Type": "application/json" }, |
| 51 | body: data ? JSON.stringify(data) : undefined |
| 52 | }).then(r => r.json()); |
| 53 | |
| 54 | const get = apiRequest("GET"); |
| 55 | const post = apiRequest("POST"); |
| 56 | const put = apiRequest("PUT"); |
| 57 | |
| 58 | const getUsers = get("/api/users"); |
| 59 | const createUser = post("/api/users"); |
| 60 | |
| 61 | // Using in pipeline |
| 62 | const loadUserPipeline = pipe( |
| 63 | id => ({ id }), |
| 64 | ({ id }) => `/api/users/${id}`, |
| 65 | get |
| 66 | ); |
info
Memoization is an optimization technique that caches the results of expensive function calls and returns the cached result when the same inputs occur again. It is a classic HOF pattern — a memoize function wraps another function and manages the cache transparently. Memoization is especially valuable for pure functions with expensive computations.
| 1 | // Generic memoize HOF |
| 2 | function memoize(fn) { |
| 3 | const cache = new Map(); |
| 4 | |
| 5 | return function(...args) { |
| 6 | const key = JSON.stringify(args); |
| 7 | if (cache.has(key)) { |
| 8 | console.log(`Cache hit: ${key}`); |
| 9 | return cache.get(key); |
| 10 | } |
| 11 | |
| 12 | const result = fn(...args); |
| 13 | cache.set(key, result); |
| 14 | console.log(`Cache miss: ${key} → ${result}`); |
| 15 | return result; |
| 16 | }; |
| 17 | } |
| 18 | |
| 19 | // Expensive computation — Fibonacci with memoization |
| 20 | const fib = memoize((n) => { |
| 21 | if (n <= 1) return n; |
| 22 | return fib(n - 1) + fib(n - 2); |
| 23 | }); |
| 24 | |
| 25 | // Without memoization: O(2^n) |
| 26 | // With memoization: O(n) |
| 27 | console.log(fib(40)); // 102334155 — fast! |
| 28 | |
| 29 | // Single-argument memoize with simpler cache |
| 30 | function memoizeSimple(fn) { |
| 31 | const cache = {}; |
| 32 | return (arg) => arg in cache ? cache[arg] : (cache[arg] = fn(arg)); |
| 33 | } |
| 34 | |
| 35 | // Memoize with TTL (time-based expiration) |
| 36 | function memoizeWithTTL(fn, ttl = 60000) { |
| 37 | const cache = new Map(); |
| 38 | |
| 39 | return function(...args) { |
| 40 | const key = JSON.stringify(args); |
| 41 | const entry = cache.get(key); |
| 42 | |
| 43 | if (entry && Date.now() - entry.timestamp < ttl) { |
| 44 | return entry.value; |
| 45 | } |
| 46 | |
| 47 | const result = fn(...args); |
| 48 | cache.set(key, { value: result, timestamp: Date.now() }); |
| 49 | return result; |
| 50 | }; |
| 51 | } |
| 52 | |
| 53 | // LRU cache memoize (least recently used, fixed size) |
| 54 | function memoizeLRU(fn, maxSize = 100) { |
| 55 | const cache = new Map(); |
| 56 | |
| 57 | return function(...args) { |
| 58 | const key = JSON.stringify(args); |
| 59 | |
| 60 | if (cache.has(key)) { |
| 61 | // Move to end (most recently used) |
| 62 | const value = cache.get(key); |
| 63 | cache.delete(key); |
| 64 | cache.set(key, value); |
| 65 | return value; |
| 66 | } |
| 67 | |
| 68 | const result = fn(...args); |
| 69 | cache.set(key, result); |
| 70 | |
| 71 | if (cache.size > maxSize) { |
| 72 | // Delete least recently used (first item) |
| 73 | const firstKey = cache.keys().next().value; |
| 74 | cache.delete(firstKey); |
| 75 | } |
| 76 | |
| 77 | return result; |
| 78 | }; |
| 79 | } |
best practice
Decorators are higher-order functions that wrap another function to add behavior without modifying the original function's code. This is an application of the decorator pattern — a structural pattern that allows behavior to be added to individual objects or functions dynamically. JavaScript's functional nature makes this pattern especially clean.
| 1 | // Decorator: logging |
| 2 | function log(fn) { |
| 3 | return function(...args) { |
| 4 | console.log(`Calling ${fn.name || "anonymous"} with:`, args); |
| 5 | const result = fn(...args); |
| 6 | console.log(`Result:`, result); |
| 7 | return result; |
| 8 | }; |
| 9 | } |
| 10 | |
| 11 | // Decorator: timing |
| 12 | function time(fn) { |
| 13 | return function(...args) { |
| 14 | const start = performance.now(); |
| 15 | try { |
| 16 | return fn(...args); |
| 17 | } finally { |
| 18 | console.log(`${fn.name} took ${performance.now() - start}ms`); |
| 19 | } |
| 20 | }; |
| 21 | } |
| 22 | |
| 23 | // Decorator: memoize |
| 24 | function memoized(fn) { |
| 25 | const cache = new Map(); |
| 26 | return function(...args) { |
| 27 | const key = JSON.stringify(args); |
| 28 | if (cache.has(key)) return cache.get(key); |
| 29 | const result = fn(...args); |
| 30 | cache.set(key, result); |
| 31 | return result; |
| 32 | }; |
| 33 | } |
| 34 | |
| 35 | // Decorator: validate |
| 36 | function validate(schema) { |
| 37 | return function(fn) { |
| 38 | return function(...args) { |
| 39 | if (schema(args)) { |
| 40 | return fn(...args); |
| 41 | } |
| 42 | throw new Error("Validation failed"); |
| 43 | }; |
| 44 | }; |
| 45 | } |
| 46 | |
| 47 | // Decorator: retry |
| 48 | function retry(maxAttempts = 3, delay = 1000) { |
| 49 | return function(fn) { |
| 50 | return async function(...args) { |
| 51 | let lastError; |
| 52 | for (let i = 0; i < maxAttempts; i++) { |
| 53 | try { |
| 54 | return await fn(...args); |
| 55 | } catch (error) { |
| 56 | lastError = error; |
| 57 | if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, delay)); |
| 58 | } |
| 59 | } |
| 60 | throw lastError; |
| 61 | }; |
| 62 | }; |
| 63 | } |
| 64 | |
| 65 | // Combining decorators |
| 66 | const getData = retry(3, 500)(time(log(async (url) => { |
| 67 | const response = await fetch(url); |
| 68 | return response.json(); |
| 69 | }))); |
| 70 | |
| 71 | // Alternative: compose decorators |
| 72 | function applyDecorators(fn, ...decorators) { |
| 73 | return decorators.reduce((f, decorator) => decorator(f), fn); |
| 74 | } |
| 75 | |
| 76 | const fetchData = applyDecorators( |
| 77 | async (url) => { |
| 78 | const res = await fetch(url); |
| 79 | return res.json(); |
| 80 | }, |
| 81 | retry(3, 500), |
| 82 | time, |
| 83 | log |
| 84 | ); |
pro tip
pipe and compose are higher-order functions that combine multiple functions into a single function. They differ only in the order of execution: pipe runs left-to-right, compose runs right-to-left. These are the fundamental tools for building data transformation pipelines in a functional style.
| 1 | // PIPE — left-to-right data flow |
| 2 | function pipe(...fns) { |
| 3 | return (initial) => fns.reduce((acc, fn) => fn(acc), initial); |
| 4 | } |
| 5 | |
| 6 | // COMPOSE — right-to-left (mathematical) |
| 7 | function compose(...fns) { |
| 8 | return (initial) => fns.reduceRight((acc, fn) => fn(acc), initial); |
| 9 | } |
| 10 | |
| 11 | // Async pipe — for Promise-returning functions |
| 12 | function pipeAsync(...fns) { |
| 13 | return (initial) => fns.reduce((acc, fn) => |
| 14 | Promise.resolve(acc).then(fn), initial |
| 15 | ); |
| 16 | } |
| 17 | |
| 18 | // Async compose |
| 19 | function composeAsync(...fns) { |
| 20 | return (initial) => fns.reduceRight((acc, fn) => |
| 21 | Promise.resolve(acc).then(fn), initial |
| 22 | ); |
| 23 | } |
| 24 | |
| 25 | // Practical: data processing pipeline |
| 26 | const fetchUser = async (id) => { |
| 27 | const res = await fetch(`/api/users/${id}`); |
| 28 | return res.json(); |
| 29 | }; |
| 30 | |
| 31 | const enrichWithPermissions = (user) => ({ |
| 32 | ...user, |
| 33 | permissions: computePermissions(user.role) |
| 34 | }); |
| 35 | |
| 36 | const formatResponse = (user) => ({ |
| 37 | data: user, |
| 38 | meta: { processedAt: Date.now() } |
| 39 | }); |
| 40 | |
| 41 | const getUserPipeline = pipeAsync( |
| 42 | fetchUser, |
| 43 | enrichWithPermissions, |
| 44 | formatResponse |
| 45 | ); |
| 46 | |
| 47 | // Conditional pipeline — add steps conditionally |
| 48 | function createPipeline(...steps) { |
| 49 | return (input) => steps.reduce((acc, step) => { |
| 50 | if (typeof step === "function") return step(acc); |
| 51 | if (step.condition && step.condition(acc)) return step.transform(acc); |
| 52 | return acc; |
| 53 | }, input); |
| 54 | } |
| 55 | |
| 56 | const processOrder = createPipeline( |
| 57 | validateOrder, |
| 58 | calculateTax, |
| 59 | { condition: (o) => o.shipping, transform: calculateShipping }, |
| 60 | applyDiscount, |
| 61 | formatInvoice |
| 62 | ); |
| 63 | |
| 64 | // Tap — side effect in a pipeline without modifying data |
| 65 | const tap = (fn) => (value) => { |
| 66 | fn(value); |
| 67 | return value; |
| 68 | }; |
| 69 | |
| 70 | const debugPipeline = pipe( |
| 71 | filter(isActive), |
| 72 | tap(arr => console.log("After filter:", arr.length)), |
| 73 | map(transform), |
| 74 | tap(arr => console.log("After map:", arr.length)), |
| 75 | reduce(summarize) |
| 76 | ); |
best practice