DNA⚡ JavaScriptFunctional Programming Patterns in JavaScript
ðŸĢHatchlingJavaScriptFunctional ProgrammingPatterns

Functional Programming Patterns in JavaScript

Pure functions, immutability, composition, currying — functional patterns that make code predictable, testable, and maintainable at scale.

Functional Programming Patterns in JavaScript

JavaScript is a multi-paradigm language, but functional patterns dominate modern frontend code. React is fundamentally functional — pure components, immutable state, composition over inheritance. Understanding these patterns is understanding the architecture of modern web apps.

Pure Functions

A pure function:

  1. Given the same inputs, always returns the same output
  2. Has no side effects (doesn't modify external state)
// Pure — same input → same output, no side effects
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
 
// Impure — modifies external state
let runningTotal = 0;
function addToTotal(amount) {
  runningTotal += amount; // Side effect!
  return runningTotal;
}
 
// Impure — depends on external state
function formatPrice(amount) {
  return `${window.currency}${amount.toFixed(2)}`; // Depends on global
}
 
// Pure version
function formatPrice(amount, currency) {
  return `${currency}${amount.toFixed(2)}`;
}

Why Purity Matters

  • Testable — No setup/teardown, no mocking globals
  • Cacheable — Same input = same output → memoization works
  • Parallelizable — No shared state → safe for Web Workers
  • Predictable — Easy to reason about, easy to debug
  • React-compatible — React requires pure render functions for concurrent mode

Immutability

Never mutate data — create new copies with changes:

// ❌ Mutation
const user = { name: 'Alice', age: 30 };
user.age = 31; // Mutates original
 
const items = [1, 2, 3];
items.push(4); // Mutates original
 
// ✅ Immutable update
const updatedUser = { ...user, age: 31 };
const updatedItems = [...items, 4];
 
// Nested update
const state = {
  user: { name: 'Alice', address: { city: 'NYC' } },
  settings: { theme: 'dark' },
};
 
const newState = {
  ...state,
  user: {
    ...state.user,
    address: { ...state.user.address, city: 'LA' },
  },
};

Immutable Array Operations

const items = [1, 2, 3, 4, 5];
 
// Add
const added = [...items, 6];                              // [1,2,3,4,5,6]
const prepended = [0, ...items];                          // [0,1,2,3,4,5]
const inserted = [...items.slice(0, 2), 99, ...items.slice(2)]; // [1,2,99,3,4,5]
 
// Remove
const removed = items.filter(x => x !== 3);               // [1,2,4,5]
const withoutIndex = items.toSpliced(2, 1);                // [1,2,4,5] (ES2023)
 
// Update
const updated = items.map(x => x === 3 ? 30 : x);        // [1,2,30,4,5]
const replaced = items.with(2, 30);                        // [1,2,30,4,5] (ES2023)
 
// Sort (immutable)
const sorted = items.toSorted((a, b) => b - a);           // [5,4,3,2,1] (ES2023)
const reversed = items.toReversed();                       // [5,4,3,2,1] (ES2023)

structuredClone — Deep Copy

const original = {
  date: new Date(),
  nested: { items: [1, [2, 3]] },
  regex: /pattern/g,
};
 
const clone = structuredClone(original);
clone.nested.items.push(4);
original.nested.items; // [1, [2, 3]] — unchanged

structuredClone handles Date, RegExp, Map, Set, ArrayBuffer, and nested objects. It does NOT clone functions, DOM nodes, or Symbols.

Higher-Order Functions

Functions that take or return functions:

// Takes a function
function withLogging(fn) {
  return function(...args) {
    console.log(`Calling ${fn.name} with`, args);
    const result = fn(...args);
    console.log(`Result:`, result);
    return result;
  };
}
 
const loggedAdd = withLogging((a, b) => a + b);
loggedAdd(2, 3); // Logs: Calling with [2, 3], Result: 5
 
// Practical: validation wrapper
function withValidation(schema, handler) {
  return function(data) {
    const errors = schema.validate(data);
    if (errors.length) throw new ValidationError(errors);
    return handler(data);
  };
}

Function Composition

Building complex operations from simple ones:

const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
 
const processUser = pipe(
  normalize,           // { name: ' ALICE ' } → { name: 'alice' }
  validate,            // Throws if invalid
  enrichWithDefaults,  // Adds missing fields
  sanitize,            // Remove dangerous content
);
 
const result = processUser(rawInput);

Practical Composition

const formatCurrency = (locale, currency) => (amount) =>
  new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
 
const addTax = (rate) => (amount) => amount * (1 + rate);
const applyDiscount = (percent) => (amount) => amount * (1 - percent / 100);
 
const calculatePrice = pipe(
  applyDiscount(10),          // 10% off
  addTax(0.08),               // 8% tax
  formatCurrency('en-US', 'USD'), // Format
);
 
calculatePrice(100); // "$97.20"

Currying

Transforming a multi-argument function into a chain of single-argument functions:

// Manual currying
const add = (a) => (b) => a + b;
const add5 = add(5);
add5(3); // 8
 
// Generic curry utility
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return function(...moreArgs) {
      return curried.apply(this, [...args, ...moreArgs]);
    };
  };
}
 
const multiply = curry((a, b, c) => a * b * c);
multiply(2)(3)(4);    // 24
multiply(2, 3)(4);    // 24
multiply(2)(3, 4);    // 24
multiply(2, 3, 4);    // 24

Partial Application (Practical Currying)

const createApiCall = (baseUrl) => (endpoint) => (options) =>
  fetch(`${baseUrl}${endpoint}`, options).then(r => r.json());
 
const api = createApiCall('https://api.example.com');
const getUsers = api('/users');
const getUser = (id) => api(`/users/${id}`)({ method: 'GET' });
 
// Event handler factories
const handleChange = (field) => (event) => {
  setState(prev => ({ ...prev, [field]: event.target.value }));
};
 
<input onChange={handleChange('name')} />
<input onChange={handleChange('email')} />

Transducers (Advanced)

Composable transformations that avoid intermediate arrays:

// Normal: creates 3 intermediate arrays
const result = data
  .filter(x => x > 10)
  .map(x => x * 2)
  .filter(x => x < 100);
 
// Transducer: single pass, no intermediates
const filterGt10 = (next) => (x) => x > 10 ? next(x) : undefined;
const double = (next) => (x) => next(x * 2);
const filterLt100 = (next) => (x) => x < 100 ? next(x) : undefined;
 
const xform = compose(filterGt10, double, filterLt100);
const result = data.reduce((acc, x) => {
  const transformed = xform((v) => v)(x);
  if (transformed !== undefined) acc.push(transformed);
  return acc;
}, []);

Monadic Patterns (Maybe/Option)

Handle null/undefined safely without null checks:

class Maybe {
  #value;
  constructor(value) { this.#value = value; }
 
  static of(value) { return new Maybe(value); }
  static empty() { return new Maybe(null); }
 
  map(fn) {
    return this.#value == null ? this : Maybe.of(fn(this.#value));
  }
 
  flatMap(fn) {
    return this.#value == null ? this : fn(this.#value);
  }
 
  getOrElse(fallback) {
    return this.#value ?? fallback;
  }
}
 
// Usage — no null checks, chain freely
const userName = Maybe.of(response)
  .map(r => r.data)
  .map(d => d.user)
  .map(u => u.name)
  .map(n => n.toUpperCase())
  .getOrElse('Anonymous');
 
// vs imperative null checking
const userName = response?.data?.user?.name?.toUpperCase() ?? 'Anonymous';

The optional chaining (?.) operator covers most cases. The Maybe pattern shines in complex pipelines where you need map/flatMap semantics.

Interview Signal

Senior candidates demonstrate:

  1. Purity reasoning — Why pure functions matter for testing, caching, concurrency, and React
  2. Immutability patterns — Spread, Array methods, structuredClone, when to use each
  3. Composition skill — pipe/compose, building complex logic from small functions
  4. Practical currying — Partial application for event handlers, API factories, configuration
  5. Trade-off awareness — FP adds abstraction; knowing when OOP or imperative is simpler