|$ curl https://forge-ai.dev/api/markdown?path=docs/js/functional
$cat docs/javascript-—-functional-programming.md
updated This week·40 min read·published

JavaScript — Functional Programming

JavaScriptFunctionalAdvancedIntermediate to Advanced
Introduction

Functional Programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. In JavaScript, FP is a first-class citizen — the language supports higher-order functions, closures, and anonymous functions, making it possible to write clean, declarative, and composable code.

FP's core tenets — pure functions, immutability, and declarative style — lead to code that is easier to reason about, test, and debug. When a function always produces the same output for the same input and has no side effects, you can understand it in isolation. When data is immutable, you never have to worry about unexpected mutations from other parts of your program.

JavaScript does not enforce FP — you can choose which concepts to adopt and to what degree. Many developers find the sweet spot is a hybrid approach: using pure functions and immutability for business logic while allowing pragmatic side effects at the system boundaries (I/O, DOM updates, network requests).

Pure Functions & Idempotence

A pure function has two properties: given the same input, it always returns the same output; and it has no side effects (it does not modify external state, perform I/O, or depend on mutable external values). Pure functions are the building blocks of functional programming.

PropertyPure FunctionImpure Function
DeterministicSame input → same output, alwaysMay depend on Date, Math.random, global state
Side effectsNone — no mutations, I/O, DOM, or loggingModifies arguments, global state, or performs I/O
TestabilityTrivial — no mocking neededRequires mocks, setup, and cleanup
CachingSafe to memoize (referential transparency)Cannot cache reliably
pure-functions.js
JavaScript
1// Pure function — deterministic, no side effects
2function add(a, b) {
3 return a + b;
4}
5
6function calculateTotal(items) {
7 return items.reduce((sum, item) => sum + item.price, 0);
8}
9
10function toUpperCase(str) {
11 return str.toUpperCase();
12}
13
14// Impure functions
15let counter = 0;
16function incrementCounter() {
17 counter++; // Side effect: modifies external state
18 return counter;
19}
20
21function getRandomDiscount(price) {
22 return price * Math.random(); // Not deterministic
23}
24
25function formatTimestamp() {
26 return new Date().toISOString(); // Depends on external state (Date.now)
27}
28
29// Idempotence — applying the operation multiple times
30// has the same effect as applying it once
31// HTTP example: PUT /users/1 { name: "Alice" }
32// Calling it once vs. ten times has the same result
33
34// Math.abs is idempotent
35console.log(Math.abs(-5)); // 5
36console.log(Math.abs(5)); // 5
37
38// Delete is idempotent
39function removeById(items, id) {
40 return items.filter(item => item.id !== id);
41}
42
43// Pure function with object spread (immutable update)
44function updateUserName(user, newName) {
45 return { ...user, name: newName };
46}

best practice

Write the core logic of your application as pure functions. Push side effects to the edges — event handlers, API callbacks, framework lifecycle methods. This isolates the most bug-prone code (side effects) from the most testable code (pure transformations). As a rule of thumb: if a function does not need side effects, keep it pure.
Immutability

Immutability means data never changes after creation. Instead of modifying an existing object or array, you create a new copy with the desired changes. This eliminates an entire class of bugs caused by unexpected mutations and makes change tracking trivial (for undo, time-travel debugging, or change detection).

immutability.js
JavaScript
1// Mutable approach (avoid in FP)
2const user = { name: 'Alice', role: 'admin' };
3user.role = 'user'; // Mutation!
4user.name = 'Bob'; // Mutation!
5
6// Immutable approach — create new objects
7const original = { name: 'Alice', role: 'admin' };
8const updated = { ...original, role: 'user' };
9console.log(original); // { name: 'Alice', role: 'admin' } — unchanged
10console.log(updated); // { name: 'Alice', role: 'user' }
11
12// Immutable arrays
13const numbers = [1, 2, 3, 4, 5];
14
15// Add — spread (prepend or append)
16const withNew = [0, ...numbers]; // [0, 1, 2, 3, 4, 5]
17const withEnd = [...numbers, 6]; // [1, 2, 3, 4, 5, 6]
18
19// Remove — filter
20const without3 = numbers.filter(n => n !== 3);
21
22// Update — map
23const doubled = numbers.map(n => n * 2);
24
25// Replace specific index
26const replaceAtIndex = (arr, index, value) => [
27 ...arr.slice(0, index),
28 value,
29 ...arr.slice(index + 1)
30];
31
32// Nested updates (deep immutability)
33const state = {
34 user: { name: 'Alice', settings: { theme: 'dark', lang: 'en' } }
35};
36
37// Update nested property
38const newState = {
39 ...state,
40 user: {
41 ...state.user,
42 settings: {
43 ...state.user.settings,
44 theme: 'light'
45 }
46 }
47};
48
49// Alternative: immer-style update (simpler syntax)
50function updateSetting(state, key, value) {
51 return {
52 ...state,
53 user: {
54 ...state.user,
55 settings: {
56 ...state.user.settings,
57 [key]: value
58 }
59 }
60 };
61}
🔥

pro tip

The spread operator (...) creates shallow copies. For deep immutability, you must copy each nested level or use a library like Immer or Immutable.js. Shallow copies are sufficient and performant for most use cases — only deeply nest what you need to change. Consider using Object.freeze() in development to detect accidental mutations (use strict mode as well).
Higher-Order Functions

A higher-order function is a function that takes one or more functions as arguments, returns a function, or both. This is possible because JavaScript treats functions as first-class citizens — they can be assigned to variables, passed as arguments, and returned from other functions.

hof.js
JavaScript
1// HOF — function that takes a function
2function withLogging(fn) {
3 return function(...args) {
4 console.log(`Calling ${fn.name || 'anonymous'}`);
5 return fn(...args);
6 };
7}
8
9const add = (a, b) => a + b;
10const loggedAdd = withLogging(add);
11console.log(loggedAdd(2, 3)); // "Calling add" then 5
12
13// HOF — function returning a function
14function multiply(factor) {
15 return function(x) {
16 return x * factor;
17 };
18}
19
20const double = multiply(2);
21const triple = multiply(3);
22console.log(double(5)); // 10
23console.log(triple(5)); // 15
24
25// Array methods as HOFs
26const numbers = [1, 2, 3, 4, 5];
27
28// map — transform each element
29const squared = numbers.map(n => n ** 2);
30
31// filter — keep elements matching predicate
32const evens = numbers.filter(n => n % 2 === 0);
33
34// reduce — accumulate values
35const sum = numbers.reduce((acc, n) => acc + n, 0);
36
37// find — get first match
38const firstLarge = numbers.find(n => n > 3);
39
40// some / every — test elements
41const hasEven = numbers.some(n => n % 2 === 0);
42const allPositive = numbers.every(n => n > 0);
43
44// Custom HOF — create predicate factory
45const greaterThan = (threshold) => (value) => value > threshold;
46const isAdult = greaterThan(18);
47console.log(isAdult(21)); // true
48console.log(isAdult(15)); // false
49
50// Custom HOF — pluck property
51const pluck = (key) => (obj) => obj[key];
52const getName = pluck('name');
53const users = [{ name: 'Alice' }, { name: 'Bob' }];
54console.log(users.map(getName)); // ['Alice', 'Bob']

info

Array methods (map, filter, reduce, find, some, every, flatMap) are the most commonly used higher-order functions in JavaScript. They replace imperative for-loops with declarative transformations. Learning to think in map/filter/reduce is the single biggest step toward writing functional JavaScript.
Function Composition

Function composition combines two or more functions to produce a new function. The output of one function becomes the input of the next. This is the essence of functional programming — building complex operations by composing simple, pure functions.

composition.js
JavaScript
1// Manual composition
2const double = (x) => x * 2;
3const increment = (x) => x + 1;
4const toString = (x) => String(x);
5
6const result = toString(increment(double(5)));
7console.log(result); // "11"
8
9// compose — right-to-left
10function compose(...fns) {
11 return function(initial) {
12 return fns.reduceRight((acc, fn) => fn(acc), initial);
13 };
14}
15
16const doubleThenIncrement = compose(increment, double);
17console.log(doubleThenIncrement(5)); // 11
18
19// pipe — left-to-right (more intuitive order)
20function pipe(...fns) {
21 return function(initial) {
22 return fns.reduce((acc, fn) => fn(acc), initial);
23 };
24}
25
26const processNumber = pipe(double, increment, toString);
27console.log(processNumber(5)); // "11"
28
29// Real-world composition: data processing pipeline
30const users = [
31 { name: 'Alice', age: 30, active: true },
32 { name: 'Bob', age: 17, active: true },
33 { name: 'Charlie', age: 25, active: false },
34 { name: 'Diana', age: 22, active: true },
35];
36
37// Define small reusable functions
38const filterActive = (users) => users.filter(u => u.active);
39const filterAdults = (users) => users.filter(u => u.age >= 18);
40const pluckNames = (users) => users.map(u => u.name);
41const sortAsc = (arr) => [...arr].sort();
42
43// Compose them
44const getActiveAdultNames = pipe(filterActive, filterAdults, pluckNames, sortAsc);
45console.log(getActiveAdultNames(users)); // ['Alice', 'Diana']
46
47// Each function is small, testable, and reusable
48// The pipeline reads like a sentence: filterActive -> filterAdults -> pluckNames -> sort
🔥

pro tip

Pipe (left-to-right) is more readable than compose (right-to-left) because it reads in the order operations execute. Libraries like Ramda provide pipe/compose out of the box. When composing many functions, consider the readability trade-off — sometimes a chain of named variables is clearer than a long pipeline.
Currying & Partial Application

Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument. Partial application pre-fills some arguments of a function, producing a function with fewer parameters. Both techniques create specialized functions from general ones.

currying.js
JavaScript
1// Uncurried function
2function add(a, b, c) {
3 return a + b + c;
4}
5
6// Curried version
7function curryAdd(a) {
8 return function(b) {
9 return function(c) {
10 return a + b + c;
11 };
12 };
13}
14
15console.log(curryAdd(1)(2)(3)); // 6
16
17// Arrow function curried
18const curriedAdd = (a) => (b) => (c) => a + b + c;
19
20// Generic curry function
21function curry(fn) {
22 return function curried(...args) {
23 if (args.length >= fn.length) {
24 return fn.apply(this, args);
25 }
26 return function(...more) {
27 return curried.apply(this, args.concat(more));
28 };
29 };
30}
31
32const curriedMultiply = curry((a, b, c) => a * b * c);
33console.log(curriedMultiply(2)(3)(4)); // 24
34console.log(curriedMultiply(2, 3)(4)); // 24
35console.log(curriedMultiply(2, 3, 4)); // 24
36
37// Partial application
38function partial(fn, ...presetArgs) {
39 return function(...laterArgs) {
40 return fn.apply(this, [...presetArgs, ...laterArgs]);
41 };
42}
43
44const add = (a, b, c) => a + b + c;
45const add5 = partial(add, 5); // Partially apply first argument
46const add5And10 = partial(add, 5, 10); // Partially apply two arguments
47
48console.log(add5(10, 15)); // 5 + 10 + 15 = 30
49console.log(add5And10(20)); // 5 + 10 + 20 = 35
50
51// Practical: creating specialized HTTP functions
52const fetchWithBase = partial(fetch, 'https://api.example.com');
53const getJSON = (url) => fetchWithBase(url).then(r => r.json());
54
55// Practical: logger with preset level
56const createLogger = (level) => (message) =>
57 console.log(`[${level.toUpperCase()}] ${message}`);
58
59const info = createLogger('info');
60const warn = createLogger('warn');
61const error = createLogger('error');
62
63info('Server started'); // [INFO] Server started
64warn('Memory high'); // [WARN] Memory high

info

Currying is common in FP libraries (Ramda, lodash/fp) but less common in everyday JavaScript. Partial application is more practical — it directly reduces function arity without restructuring how the function is called. Use currying when building pipelines with compose/pipe; use partial for concrete APIs like event handlers or callbacks.
Recursion Patterns

Recursion is a technique where a function calls itself to solve a smaller instance of the same problem. In functional programming, recursion replaces iterative loops (for, while) since those involve mutable counters. JavaScript engines implement tail call optimization (TCO) in strict mode, though support varies.

recursion.js
JavaScript
1// Basic recursion — factorial
2function factorial(n) {
3 if (n <= 1) return 1; // Base case
4 return n * factorial(n - 1); // Recursive case
5}
6
7// Recursion — array sum
8function sum(arr) {
9 if (arr.length === 0) return 0;
10 return arr[0] + sum(arr.slice(1));
11}
12
13// Tail-recursive version (optimized with TCO in strict mode)
14function sumTail(arr, acc = 0) {
15 if (arr.length === 0) return acc;
16 return sumTail(arr.slice(1), acc + arr[0]);
17}
18
19// Recursion — tree traversal
20const tree = {
21 value: 1,
22 children: [
23 { value: 2, children: [] },
24 {
25 value: 3,
26 children: [
27 { value: 4, children: [] },
28 { value: 5, children: [] }
29 ]
30 }
31 ]
32};
33
34function deepFlatten(node) {
35 let values = [node.value];
36 for (const child of node.children) {
37 values = values.concat(deepFlatten(child));
38 }
39 return values;
40}
41
42console.log(deepFlatten(tree)); // [1, 2, 3, 4, 5]
43
44// Recursion — deep object search
45function findInTree(node, predicate) {
46 if (predicate(node)) return node;
47 for (const child of node.children) {
48 const found = findInTree(child, predicate);
49 if (found) return found;
50 }
51 return null;
52}
53
54// Recursion — DOM traversal
55function walkDOM(node, callback) {
56 callback(node);
57 node = node.firstChild;
58 while (node) {
59 walkDOM(node, callback);
60 node = node.nextSibling;
61 }
62}
63
64walkDOM(document.body, (el) => {
65 if (el.nodeType === 1) {
66 console.log(el.tagName);
67 }
68});
69
70// Mutual recursion — even/odd check (demonstration)
71function isEven(n) {
72 if (n === 0) return true;
73 return isOdd(n - 1);
74}
75
76function isOdd(n) {
77 if (n === 0) return false;
78 return isEven(n - 1);
79}
80
81console.log(isEven(4)); // true
82console.log(isOdd(7)); // true

warning

JavaScript engines limit recursion depth (typically ~10,000 frames). For deeply recursive operations, use trampolining or convert to iteration. Tail call optimization (TCO) is part of the ES2015 spec but only implemented in Safari (JavaScriptCore). In V8 (Chrome, Node), TCO was proposed but never shipped. Write recursive code for clarity, but be aware of stack limits.
Functors & Monads

Functors and Monads are abstractions from category theory that have practical applications in JavaScript. A Functor is a container that implements a map method — you can transform the contained value without leaving the container. A Monad is a Functor that also implements flatMap (or chain), allowing you to avoid nested containers.

Maybe Monad

The Maybe monad represents an optional value that may or may not exist (avoiding null checks). It has two variants: Just (a value exists) and Nothing (no value). Operations on Nothing are safely ignored.

monads.js
JavaScript
1// Maybe monad implementation
2class Maybe {
3 static just(value) { return new Just(value); }
4 static nothing() { return new Nothing(); }
5 static of(value) {
6 return value == null ? Maybe.nothing() : Maybe.just(value);
7 }
8
9 isNothing() { return false; }
10 isJust() { return false; }
11}
12
13class Just extends Maybe {
14 constructor(value) { super(); this._value = value; }
15
16 map(fn) { return Maybe.of(fn(this._value)); }
17 flatMap(fn) { return fn(this._value); }
18 getOrElse(defaultValue) { return this._value; }
19 get() { return this._value; }
20}
21
22class Nothing extends Maybe {
23 map(fn) { return this; }
24 flatMap(fn) { return this; }
25 getOrElse(defaultValue) { return defaultValue; }
26 get() { throw new Error('Cannot get value from Nothing'); }
27}
28
29// Usage — safe property access
30function getConfigValue(config, path) {
31 return path.split('.').reduce(
32 (acc, key) => acc.flatMap(obj => Maybe.of(obj[key])),
33 Maybe.of(config)
34 );
35}
36
37const config = {
38 database: { host: 'localhost', port: 5432 }
39};
40
41const host = getConfigValue(config, 'database.host');
42console.log(host.getOrElse('default-host')); // localhost
43
44const missing = getConfigValue(config, 'database.password');
45console.log(missing.getOrElse('default-password')); // default-password
46
47// Safe computation pipeline
48function safeParseJSON(str) {
49 try {
50 return Maybe.just(JSON.parse(str));
51 } catch {
52 return Maybe.nothing();
53 }
54}
55
56function safeGetUser(id) {
57 // Simulated API call
58 const users = { 1: { name: 'Alice' } };
59 return Maybe.of(users[id]);
60}
61
62const result = safeGetUser(1)
63 .map(user => user.name)
64 .getOrElse('Unknown');
65console.log(result); // Alice
66
67// Either monad (success/failure with context)
68class Either {
69 static left(value) { return new Left(value); }
70 static right(value) { return new Right(value); }
71 static of(value) { return Either.right(value); }
72}
73
74class Left extends Either {
75 constructor(value) { super(); this._value = value; }
76 map(fn) { return this; }
77 flatMap(fn) { return this; }
78 getOrElse(defaultValue) { return defaultValue; }
79 get() { throw new Error(this._value); }
80}
81
82class Right extends Either {
83 constructor(value) { super(); this._value = value; }
84 map(fn) { return Either.of(fn(this._value)); }
85 flatMap(fn) { return fn(this._value); }
86 getOrElse(defaultValue) { return this._value; }
87 get() { return this._value; }
88}
89
90// Either for error handling
91function divide(a, b) {
92 if (b === 0) return Either.left('Division by zero');
93 return Either.right(a / b);
94}
95
96const result1 = divide(10, 2)
97 .map(x => x * 2)
98 .getOrElse(0);
99console.log(result1); // 10
100
101const result2 = divide(10, 0)
102 .map(x => x * 2)
103 .getOrElse(0);
104console.log(result2); // 0 (safe, no crash)
📝

note

Monads in JavaScript are less common than in languages like Haskell, but the pattern is useful for chaining nullable operations (Maybe), error handling (Either), and async computations (Promise is technically a monad). You do not need the formal terminology — the practical pattern is simple: a wrapper that lets you chain operations while handling edge cases automatically.
Advanced FP Concepts

Lazy Evaluation

Lazy evaluation delays computation until the result is actually needed. This enables working with infinite data structures and avoids unnecessary work. JavaScript is normally eager, but generators and thunks enable lazy patterns.

lazy.js
JavaScript
1// Lazy evaluation with generators
2function* fibonacci() {
3 let a = 0, b = 1;
4 while (true) {
5 yield a;
6 [a, b] = [b, a + b];
7 }
8}
9
10const fib = fibonacci();
11console.log(fib.next().value); // 0
12console.log(fib.next().value); // 1
13console.log(fib.next().value); // 1
14console.log(fib.next().value); // 2
15console.log(fib.next().value); // 3
16// Values computed only when requested
17
18// Lazy range
19function* range(start, end, step = 1) {
20 let current = start;
21 while (current < end) {
22 yield current;
23 current += step;
24 }
25}
26
27// Lazy map
28function* lazyMap(iterable, fn) {
29 for (const item of iterable) {
30 yield fn(item);
31 }
32}
33
34// Lazy filter
35function* lazyFilter(iterable, predicate) {
36 for (const item of iterable) {
37 if (predicate(item)) yield item;
38 }
39}
40
41// Compose lazy operations — no intermediate arrays
42const numbers = range(0, 1000000);
43const evenSquares = lazyMap(
44 lazyFilter(numbers, n => n % 2 === 0),
45 n => n ** 2
46);
47
48// Only the first 5 values are computed
49for (const val of evenSquares) {
50 if (val > 100) break;
51 console.log(val); // 4, 16, 36, 64, 100
52}
53
54// Thunk — deferred computation
55const expensive = () => {
56 console.log('Computing...');
57 return 42;
58};
59
60const thunk = () => expensive(); // Nothing computed yet
61console.log('Before'); // "Before"
62const result = thunk(); // "Computing..."
63console.log(result); // 42
64
65// Memoized thunk (lazy with caching)
66function lazy(fn) {
67 let evaluated = false;
68 let result;
69 return () => {
70 if (!evaluated) {
71 result = fn();
72 evaluated = true;
73 }
74 return result;
75 };
76}
77
78const config = lazy(() => {
79 console.log('Loading config...');
80 return JSON.parse(localStorage.getItem('config') || '{}');
81});
82
83// Config loaded only on first access
84console.log(config()); // "Loading config..." then {}
85console.log(config()); // Cached, no loading

Transducers

Transducers are composable, efficient transformations that work across different data types (arrays, streams, observables). They avoid creating intermediate collections by combining map/filter/reduce into a single pass.

transducers.js
JavaScript
1// The problem: intermediate arrays
2const nums = [1, 2, 3, 4, 5, 6];
3const result = nums
4 .filter(n => n % 2 === 0) // [2, 4, 6] — intermediate
5 .map(n => n * 10) // [20, 40, 60] — intermediate
6 .slice(0, 2); // [20, 40] — final
7
8// Transducer approach: single pass
9function mapping(fn) {
10 return (reducer) => (acc, value) => reducer(acc, fn(value));
11}
12
13function filtering(predicate) {
14 return (reducer) => (acc, value) =>
15 predicate(value) ? reducer(acc, value) : acc;
16}
17
18function taking(n) {
19 let count = 0;
20 return (reducer) => (acc, value) => {
21 if (count >= n) return acc;
22 count++;
23 return reducer(acc, value);
24 };
25}
26
27function compose(...fns) {
28 return fns.reduce((f, g) => (...args) => f(g(...args)));
29}
30
31function transduce(transducer, reducer, initial, collection) {
32 const transformedReducer = transducer(reducer);
33 return collection.reduce(transformedReducer, initial);
34}
35
36// Compose the transducer (applied right-to-left in compose)
37const process = compose(taking(2), mapping(n => n * 10), filtering(n => n % 2 === 0));
38
39const pushReducer = (acc, value) => { acc.push(value); return acc; };
40
41const result2 = transduce(process, pushReducer, [], nums);
42console.log(result2); // [20, 40] — single pass, no intermediates
43
44// Transducer works with any data structure
45// Same transformation on a Set
46const numSet = new Set([1, 2, 3, 4, 5, 6]);
47const result3 = transduce(process, (acc, v) => acc.add(v), new Set(), numSet);
48console.log(result3); // Set { 20, 40 }
🔥

pro tip

Transducers are an advanced FP concept. In practice, they are most useful when processing large datasets where intermediate arrays would cause memory pressure. For everyday code, the simplicity of chainable array methods outweighs the performance gain. Libraries like Ramda and transducers-js provide production-ready transducer implementations.
FP Libraries Comparison

Several libraries extend JavaScript's FP capabilities. Each takes a different approach — Ramda emphasizes point-free style and composition, lodash/fp provides auto-curried FP versions of lodash functions, and Immutable.js focuses on persistent data structures.

FeatureRamdalodash/fpImmutable.jsNative JS
CurryingAuto-curried, data-lastAuto-curried, data-lastN/AManual only
Compositioncompose, pipeflow, composeN/AManual
ImmutabilityShallow (functions return new refs)Shallow (clone/mutate pattern)Deep (persistent data structures)Spread / Object.assign
Bundle size~15KB (tree-shakeable)~24KB (lodash full)~63KB0KB
Data structuresPlain arrays/objectsPlain arrays/objectsMap, List, Set, Record, StackPlain arrays/objects
Learning curveModerate (point-free style)Low (lodash familiarity)Moderate (new API)None
fp-libraries.js
JavaScript
1// Ramda example (point-free, data-last)
2import R from 'ramda';
3
4const getActiveNames = R.pipe(
5 R.filter(R.propEq('active', true)),
6 R.map(R.prop('name')),
7 R.sortBy(R.identity)
8);
9
10// lodash/fp example (auto-curried, data-last)
11import _ from 'lodash/fp';
12
13const getActiveNamesFP = _.flow(
14 _.filter({ active: true }),
15 _.map('name'),
16 _.sortBy(_.identity)
17);
18
19// Immutable.js — persistent data structures
20import { Map, List, fromJS } from 'immutable';
21
22const state = fromJS({
23 user: { name: 'Alice', settings: { theme: 'dark' } }
24});
25
26// Deep update without mutation
27const newState = state.setIn(['user', 'settings', 'theme'], 'light');
28console.log(newState.getIn(['user', 'settings', 'theme'])); // light
29console.log(state.getIn(['user', 'settings', 'theme'])); // dark (unchanged)
30
31// List operations
32const list = List([1, 2, 3]);
33const updated = list.push(4).map(n => n * 2);
34console.log(updated.toJS()); // [2, 4, 6, 8]
35
36// Performance: structural sharing
37// Immutable.js uses tries — only the changed path is copied
38// The rest is shared with the original structure

best practice

Start with native JavaScript — map, filter, reduce, spread operators, and destructuring cover most FP needs. Add Ramda when you need point-free composition across a large codebase. Add lodash/fp if you already use lodash. Add Immutable.js only when you need structural sharing for performance (large state trees, time-travel debugging). Avoid libraries for small projects — the dependency cost often outweighs the benefit.
Best Practices
Write pure functions by default — extract side effects to the edges of your application (event handlers, framework callbacks, I/O boundaries)
Use const over let — it signals that a reference does not change (though it does not guarantee immutability of the value itself)
Prefer expressions over statements — ternary, &&, ||, and arrow function expressions are more composable than if/else blocks
Avoid mutating function parameters — treat all arguments as read-only; create new objects/arrays for output
Use array methods (map, filter, reduce, flatMap) instead of imperative loops — they are declarative and chainable
Keep functions small and single-purpose — a function should do one thing and have a clear name describing that thing
Favor shallow immutability with spread/rest — it is simpler and more performant than deep cloning for most cases
Avoid null/undefined — use Maybe or provide default values. Consider using the optional chaining (?.) and nullish coalescing (??) operators
Measure before optimizing — pure FP techniques like transducers add complexity. Profile your app before adopting them
Remember that pragmatism beats purity — FP is a tool, not a religion. A pragmatic mutation inside a local scope is better than a contorted pure solution
🔥

pro tip

The most impactful FP technique for most JavaScript developers is simply writing smaller, pure functions and using map/filter/reduce instead of for-loops. Do not feel pressured to adopt monads, transducers, or point-free style. Start with the basics — purity and immutability — and gradually explore advanced concepts as your problem domain requires them.
$Blueprint — Engineering Documentation·Section ID: JS-FP·Revision: 1.0