DevToolBoxZA DARMO
Blog

Functional Programming Guide: Pure Functions, Immutability, Monads, Composition & FP in JavaScript/TypeScript

20 min readby DevToolBox Team

Functional Programming Guide: Pure Functions, Immutability, Monads & More

Master functional programming concepts: pure functions, immutability, higher-order functions, closures, currying, composition, monads, functors, pattern matching, and algebraic data types — with JavaScript/TypeScript and Haskell/Scala comparisons.

TL;DR — Functional Programming in 60 Seconds
  • Pure functions: same input → same output, no side effects — cacheable & parallelizable
  • Immutability: data never changes after creation; use spread, Immer, or persistent data structures
  • Higher-order functions: map, filter, reduce are core tools; functions are first-class citizens
  • Currying & composition: transform multi-arg functions into unary chains, build pipelines with pipe/compose
  • Monads (Maybe, Either, Promise): handle nulls, errors, and async in a composable way
  • ADTs + pattern matching: make illegal states unrepresentable
  • Haskell is the pure FP gold standard; TypeScript supports FP style through libraries and conventions
Key Takeaways
  • FP is a declarative paradigm emphasizing expressions over statements
  • Referential transparency makes code reason-able like math equations
  • TypeScript's Readonly, as const, discriminated unions are practical FP tools
  • fp-ts, Effect-TS, Ramda, and Lodash/fp bring FP to the JS/TS ecosystem
  • Scala blends OOP and FP; Haskell enforces pure functional style
  • Learning FP concepts improves code quality across all paradigms

1. Pure Functions & Referential Transparency

Pure functions are the cornerstone of functional programming. A function is "pure" if and only if: (1) it always returns the same output for the same input, and (2) it produces no observable side effects — no mutation of external state, no I/O, no dependency on randomness or time. Referential transparency means you can replace a function call with its return value without changing program behavior.

Pure Functions in JavaScript/TypeScript

// Pure: same input → same output, no side effects
const add = (a: number, b: number): number => a + b;
const toUpper = (s: string): string => s.toUpperCase();

// Impure: depends on external state
let tax = 0.1;
const calcPrice = (price: number) => price * (1 + tax); // reads external var

// Impure: side effect (mutates input)
const addItem = (arr: number[], item: number) => {
  arr.push(item); // mutates original array!
  return arr;
};

// Pure alternative: returns new array
const addItemPure = (arr: readonly number[], item: number): number[] => [
  ...arr,
  item,
];

Pure Functions in Haskell

-- All Haskell functions are pure by default
-- Side effects are tracked by the type system (IO monad)

add :: Int -> Int -> Int
add a b = a + b

double :: Int -> Int
double = (*2)  -- point-free style

-- Referential transparency:
-- double 5 can always be replaced with 10
-- This enables equational reasoning and compiler optimizations

2. Immutability & Persistent Data Structures

Immutable data cannot be modified after creation. Any "change" produces a new copy. This eliminates bugs from shared mutable state, makes concurrency safe, and simplifies undo/redo features. Persistent data structures use structural sharing to efficiently create "modified" copies, avoiding the performance cost of full copies.

Immutability in TypeScript

// 1. Object.freeze — shallow immutability
const config = Object.freeze({ host: "localhost", port: 3000 });
// config.port = 8080; // TypeError in strict mode

// 2. Readonly<T> — compile-time enforcement
interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
}

// 3. ReadonlyArray and as const
const colors = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]

// 4. Spread-based updates (pure)
const updateUser = (user: User, name: string): User => ({
  ...user,
  name,
});

// 5. Immer — mutable API, immutable result
import { produce } from "immer";
const nextState = produce(state, (draft) => {
  draft.users[0].name = "Alice"; // looks mutable, but produces new object
});

Default Immutability in Haskell/Scala

-- Haskell: all values are immutable by default
let xs = [1, 2, 3]
let ys = 0 : xs          -- [0, 1, 2, 3] — xs is unchanged
let zs = map (*2) xs     -- [2, 4, 6]   — xs is unchanged

// Scala: val (immutable) vs var (mutable)
val xs = List(1, 2, 3)          // immutable List
val ys = 0 :: xs                // List(0, 1, 2, 3)
val zs = xs.map(_ * 2)          // List(2, 4, 6)

// Scala case classes are immutable by default
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31) // new instance, alice unchanged

3. Higher-Order Functions

Higher-order functions either take functions as arguments, return functions, or both. They are the primary mechanism for code reuse in functional programming. map, filter, and reduce are the three most fundamental higher-order functions — nearly all data transformations can be expressed through them.

// TypeScript higher-order functions
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map: transform each element
const doubled = numbers.map((n) => n * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter: select elements matching predicate
const evens = numbers.filter((n) => n % 2 === 0);
// [2, 4, 6, 8, 10]

// reduce: fold into single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 55

// Returning a function (function factory)
const multiplier = (factor: number) => (n: number) => n * factor;
const triple = multiplier(3);
triple(7); // 21

// Generic higher-order function
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}
const lengths = mapArray(["hello", "world"], (s) => s.length);
// [5, 5]

Higher-Order Functions in Haskell

-- Haskell: functions are curried by default
map (*2) [1..10]          -- [2,4,6,8,10,12,14,16,18,20]
filter even [1..10]       -- [2,4,6,8,10]
foldl (+) 0 [1..10]       -- 55

-- Function composition with (.)
sumOfDoubledEvens :: [Int] -> Int
sumOfDoubledEvens = foldl (+) 0 . map (*2) . filter even

-- ($) eliminates parentheses
print $ map (*2) $ filter even [1..10]
-- equivalent to: print (map (*2) (filter even [1..10]))

-- Custom higher-order function
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
applyTwice (+3) 10   -- 16
applyTwice (*2) 3    -- 12

4. Closures

A closure is a function bundled together with references to variables from its surrounding scope. The closure "captures" the environment at creation time — captured variables remain alive even after the outer function returns. Closures are the foundation of currying, partial application, and the module pattern.

// Closure: inner function captures variables from outer scope
function makeCounter(initial = 0) {
  let count = initial; // captured by returned functions
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  };
}
const counter = makeCounter(10);
counter.increment(); // 11
counter.increment(); // 12

// Closure for configuration (curried logger)
const createLogger = (prefix: string) => (msg: string) =>
  console.log(`[${prefix}] ${msg}`);

const dbLog = createLogger("DB");
dbLog("Connected");  // [DB] Connected

5. Currying & Partial Application

Currying transforms a multi-argument function into a chain of single-argument functions. Partial application fixes some arguments and returns a new function expecting the remaining ones. In Haskell, all functions are automatically curried; in JavaScript/TypeScript, you need to implement it manually or use a library.

// Manual currying in TypeScript
const add = (a: number) => (b: number) => a + b;
const add5 = add(5);
add5(3);  // 8
add(2)(3); // 5

// Generic curry function (for 2-arg functions)
function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
  return (a: A) => (b: B) => fn(a, b);
}

const curriedMultiply = curry((a: number, b: number) => a * b);
const double = curriedMultiply(2);
double(10); // 20

// Partial application — fix first argument
function partial<A, B extends unknown[], C>(
  fn: (a: A, ...rest: B) => C,
  a: A
): (...rest: B) => C {
  return (...rest: B) => fn(a, ...rest);
}

const log = (level: string, module: string, msg: string) =>
  `[${level}][${module}] ${msg}`;

const errorLog = partial(log, "ERROR");
errorLog("DB", "Connection failed");
// [ERROR][DB] Connection failed

Haskell's Automatic Currying

-- All functions are curried: add :: Int -> Int -> Int = Int -> (Int -> Int)
add a b = a + b
add5 = add 5             -- partial application is trivial
add5 3                   -- 8

-- Sections: partially apply operators
map (*2) [1,2,3]         -- [2,4,6]
filter (>0) [-2,0,3,5]   -- [3,5]

6. Function Composition & Pipelines

Function composition combines two or more functions into a new function where the output of one becomes the input of the next. compose(f, g)(x) = f(g(x)) executes right-to-left, while pipe(f, g)(x) = g(f(x)) executes left-to-right. Composition lets you build complex transformations like building blocks.

// compose: right-to-left
const compose = <T>(...fns: Array<(arg: T) => T>) =>
  (x: T): T => fns.reduceRight((acc, fn) => fn(acc), x);

// pipe: left-to-right (more readable)
const pipe = <T>(...fns: Array<(arg: T) => T>) =>
  (x: T): T => fns.reduce((acc, fn) => fn(acc), x);

// Building a text processing pipeline
const trim = (s: string) => s.trim();
const toLower = (s: string) => s.toLowerCase();
const replaceSpaces = (s: string) => s.replace(/\s+/g, "-");

const slugify = pipe(trim, toLower, replaceSpaces);

slugify("  Hello World  "); // "hello-world"

Composition in Haskell

-- (.) is the composition operator: (f . g) x = f (g x)
sumOfSquares :: [Int] -> Int
sumOfSquares = sum . map (^2)   -- point-free style

-- ($) eliminates parentheses: f $ x = f x
main = print $ sumOfSquares [1,2,3,4,5]  -- 55

-- Scala: f andThen g (left-to-right) or f compose g (right-to-left)

7. Monads: Managing Side Effects & Chained Computation

A Monad is a design pattern for composing computations that carry context — possibly null, possibly failed, asynchronous, etc. A Monad needs two operations: unit (also called return / of — wrapping a value) and flatMap (also called bind / chain / >>= — applying a function to a wrapped value and flattening the result). Promise is the most common Monad in JavaScript.

Maybe Monad (Handling Nulls)

// Maybe monad in TypeScript
type Maybe<T> = { tag: "Just"; value: T } | { tag: "Nothing" };

const Just = <T>(value: T): Maybe<T> => ({ tag: "Just", value });
const Nothing = <T>(): Maybe<T> => ({ tag: "Nothing" });

// unit: wrap a value
const of = <T>(value: T | null | undefined): Maybe<T> =>
  value != null ? Just(value) : Nothing();

// flatMap (bind): chain computations
const flatMap = <T, U>(
  maybe: Maybe<T>,
  fn: (value: T) => Maybe<U>
): Maybe<U> =>
  maybe.tag === "Just" ? fn(maybe.value) : Nothing();

// map: apply function without flattening
const map = <T, U>(maybe: Maybe<T>, fn: (value: T) => U): Maybe<U> =>
  flatMap(maybe, (v) => Just(fn(v)));

// Usage: safe chained property access
const getCity = (co: { address?: { city?: string } }): Maybe<string> =>
  flatMap(of(co.address), (addr) => of(addr.city));

getCity({ address: { city: "NYC" } }); // { tag: "Just", value: "NYC" }
getCity({});                             // { tag: "Nothing" }

Either Monad (Error Handling)

// Either: Right = success, Left = error
type Either<E, A> =
  | { tag: "Left"; error: E }
  | { tag: "Right"; value: A };

const Left = <E, A>(error: E): Either<E, A> => ({ tag: "Left", error });
const Right = <E, A>(value: A): Either<E, A> => ({ tag: "Right", value });

const flatMap = <E, A, B>(
  either: Either<E, A>, fn: (a: A) => Either<E, B>
): Either<E, B> =>
  either.tag === "Right" ? fn(either.value) : either;

// Railway-oriented programming: chain validations
const parseAge = (s: string): Either<string, number> => {
  const n = parseInt(s, 10);
  return isNaN(n) ? Left("Invalid number") : Right(n);
};
const validateRange = (n: number): Either<string, number> =>
  n >= 0 && n <= 150 ? Right(n) : Left("Out of range");

flatMap(parseAge("25"), validateRange); // { tag: "Right", value: 25 }

Monads in Haskell

-- Monad typeclass: return + (>>=)
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)

-- do notation (syntactic sugar for >>=)
result :: Maybe Double
result = do
  x <- Just 100
  y <- safeDivide x 5
  safeDivide y 2         -- Just 10.0

-- desugared: Just 100 >>= \x -> safeDivide x 5 >>= \y -> safeDivide y 2

8. Functors: Mappable Containers

A Functor is a container type that implements a map operation, transforming inner values without changing the container's structure. Arrays, Maybe, Either, and Promise are all Functors. Functors must obey two laws: identity (map(id) === id) and composition (map(f . g) === map(f) . map(g)).

// Functor interface in TypeScript
interface Functor<A> {
  map<B>(fn: (a: A) => B): Functor<B>;
}

// Box functor: simplest possible container
class Box<A> implements Functor<A> {
  constructor(private value: A) {}

  map<B>(fn: (a: A) => B): Box<B> {
    return new Box(fn(this.value));
  }

  getValue(): A {
    return this.value;
  }
}

// Chain transformations
const result = new Box(5)
  .map((n) => n * 2)      // Box(10)
  .map((n) => n + 1)      // Box(11)
  .map(String)             // Box("11")
  .getValue();             // "11"

// Functor laws verification:
// 1. Identity: box.map(x => x) ≡ box
// 2. Composition: box.map(f).map(g) ≡ box.map(x => g(f(x)))

Functors in Haskell

-- class Functor f where fmap :: (a -> b) -> f a -> f b

fmap (*2) (Just 5)      -- Just 10
fmap (*2) Nothing       -- Nothing
fmap (*2) [1,2,3]       -- [2,4,6]

-- <$> is infix fmap
(*2) <$> Just 5         -- Just 10

-- Custom Functor: derive fmap for your own types
data Tree a = Leaf a | Branch (Tree a) (Tree a)
instance Functor Tree where
  fmap f (Leaf x)     = Leaf (f x)
  fmap f (Branch l r) = Branch (fmap f l) (fmap f r)

9. Pattern Matching

Pattern matching is a mechanism for checking a value's shape and destructuring data. It is more powerful than if/else or switch because it combines type checking, destructuring, and conditional logic simultaneously. Haskell and Scala have built-in pattern matching with exhaustiveness checking. TypeScript achieves similar results through discriminated unions with switch.

TypeScript Discriminated Unions

// Algebraic Data Type via discriminated union
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

// Exhaustive pattern matching
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      // Exhaustiveness check: this will error if a case is missed
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Result type: another common discriminated union
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

Pattern Matching in Haskell

data Shape = Circle Double | Rectangle Double Double | Triangle Double Double

area :: Shape -> Double
area (Circle r)      = pi * r^2
area (Rectangle w h) = w * h
area (Triangle b h)  = 0.5 * b * h

-- Guards: conditions within pattern matches
bmi :: Double -> String
bmi x | x < 18.5 = "Underweight" | x < 25.0 = "Normal" | otherwise = "Overweight"

-- Nested pattern matching
describe :: Maybe (Either String Int) -> String
describe Nothing          = "Empty"
describe (Just (Left s))  = "Error: " ++ s
describe (Just (Right n)) = "Value: " ++ show n

10. Algebraic Data Types (ADTs)

Algebraic data types combine types in two ways: product types (AND — records/tuples, all fields present) and sum types (OR — tagged unions, exactly one variant). The key advantage of ADTs is "making illegal states unrepresentable" — using the type system to enforce constraints that catch runtime errors at compile time.

// Product type: all fields present (AND)
type User = {
  id: string;
  name: string;
  email: string;
};

// Sum type: exactly one variant (OR)
type LoadingState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

// Making illegal states unrepresentable
// BAD: both data and error could be set simultaneously
type BadState = {
  loading: boolean;
  data: string | null;
  error: Error | null;
};

// GOOD: exactly one state at a time
function renderState<T>(state: LoadingState<T>): string {
  switch (state.status) {
    case "idle":    return "Ready";
    case "loading": return "Loading...";
    case "success": return `Data: ${String(state.data)}`;
    case "error":   return `Error: ${state.error.message}`;
  }
}

Haskell ADTs

-- Sum type (OR)
data Color = Red | Green | Blue

-- Product type (AND)
data Point = Point { x :: Double, y :: Double }

-- Parameterized ADTs
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b

-- Recursive ADT (binary tree)
data Tree a = Empty | Node a (Tree a) (Tree a)

-- Pattern match on ADTs
depth :: Tree a -> Int
depth Empty          = 0
depth (Node _ l r)   = 1 + max (depth l) (depth r)

11. Practical FP Patterns & Libraries

In real projects, you likely will not implement Maybe and Either from scratch. Here are the mainstream functional programming libraries in the JavaScript/TypeScript ecosystem that provide battle-tested functional utilities.

LibraryFeaturesBest For
fp-tsOption, Either, Task, IO, pipe, type classesHaskell-style strict FP
Effect-TSEffect system, DI, concurrency, streamsSide-effect management in large apps
RamdaAuto-curry, data-last, lensesData transformation pipelines
Lodash/fpFP build of Lodash, auto-curried, immutableProjects already using Lodash
ImmerStructural sharing, mutable API → immutable resultReact state management
ZodRuntime validation, parse-don't-validateAPI boundary validation

fp-ts in Practice

import { pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";
import * as E from "fp-ts/Either";

// Option: safe property access
const getCity = (user: User): O.Option<string> =>
  pipe(
    O.fromNullable(user.address),
    O.flatMap((addr) => O.fromNullable(addr.city))
  );

// Either: validation pipeline
const validateInput = (input: string) =>
  pipe(
    E.right(input),
    E.flatMap((s) => s.length >= 5 ? E.right(s) : E.left("Too short")),
    E.flatMap((s) => s.includes("@") ? E.right(s) : E.left("Invalid email"))
  );

12. Language Comparison: TypeScript vs Haskell vs Scala

The table below compares how three languages support functional programming. Haskell is a pure functional language, Scala is a multi-paradigm JVM language, and TypeScript is a multi-paradigm JavaScript superset with good FP support.

FeatureTypeScriptHaskellScala
Pure FPConvention, not enforcedEnforced (IO monad)Optional, not enforced
ImmutabilityReadonly, as constDefault immutableval, case class
Type InferenceLocal inferenceHindley-MilnerLocal + advanced
Pattern Matchingswitch + neverBuilt-in + exhaustivematch expression
CurryingManual or libraryAuto-curried by defaultMultiple param lists
Type ClassesNone (interfaces)First-class supportgiven/using (Scala 3)
ADTsDiscriminated unionsdata + |sealed trait + case class
Learning CurveLowHighMedium

13. Advanced Patterns: Lenses, Optics & Transducers

A Lens is a composable getter/setter abstraction for elegantly reading and updating deep fields in immutable nested structures. Optics is the broader concept, including Prisms (for sum types), Traversals (for multiple targets), and more. Transducers are composable transformations independent of the data source.

// Lens: composable getter/setter for nested immutable data
type Lens<S, A> = {
  get: (s: S) => A;
  set: (a: A) => (s: S) => S;
};

const lensProp = <S, K extends keyof S>(key: K): Lens<S, S[K]> => ({
  get: (s) => s[key],
  set: (a) => (s) => ({ ...s, [key]: a }),
});

// Compose lenses for deep nested access
const composeLens = <A, B, C>(
  ab: Lens<A, B>, bc: Lens<B, C>
): Lens<A, C> => ({
  get: (a) => bc.get(ab.get(a)),
  set: (c) => (a) => ab.set(bc.set(c)(ab.get(a)))(a),
});

// Usage: update deeply nested field immutably
const addressLens = lensProp<Company, "address">("address");
const cityLens = lensProp<Company["address"], "city">("city");
const companyCityLens = composeLens(addressLens, cityLens);

companyCityLens.get(company);           // "NYC"
companyCityLens.set("LA")(company);     // { ...company, address: { ...city: "LA" } }

14. Recursion & Tail Call Optimization

Functional programming replaces loops with recursion. Tail recursion (where the recursive call is the last operation) can be optimized by the compiler to use constant stack space. Haskell and Scala support tail call optimization; JavaScript only supports TCO in Safari. In JS/TS, you can use the trampoline pattern to simulate TCO.

// Naive recursion: O(n) stack space — can overflow
const factorial = (n: number): number =>
  n <= 1 ? 1 : n * factorial(n - 1);

// Tail-recursive version: last operation is the recursive call
const factorialTail = (n: number, acc: number = 1): number =>
  n <= 1 ? acc : factorialTail(n - 1, n * acc);

// Trampoline: simulate TCO in JS environments without it
type Thunk<T> = T | (() => Thunk<T>);

function trampoline<T>(fn: Thunk<T>): T {
  let result = fn;
  while (typeof result === "function") {
    result = (result as () => Thunk<T>)();
  }
  return result;
}

const factTrampoline = (n: number, acc = 1): Thunk<number> =>
  n <= 1 ? acc : () => factTrampoline(n - 1, n * acc);

trampoline(factTrampoline(100000)); // Works without stack overflow!

Recursion in Haskell/Scala

-- Haskell: tail-recursive with accumulator
factTail :: Integer -> Integer
factTail n = go n 1
  where go 0 acc = acc
        go n acc = go (n-1) (n*acc)

// Scala: @tailrec ensures compiler optimizes tail calls
import scala.annotation.tailrec
@tailrec
def factorial(n: Long, acc: Long = 1): Long =
  if (n <= 1) acc else factorial(n - 1, n * acc)

15. Functional Programming & React

React's core design is deeply influenced by functional programming: components are pure functions (props to JSX), state is immutable (updated via setState/useReducer), Hooks are higher-order function patterns, and React 18/19's Suspense and Server Components embody Effect system ideas.

// React: FP patterns everywhere

// 1. Pure component: function from props to UI
const Greeting = ({ name }: { name: string }) => <h1>Hello, {name}!</h1>;

// 2. Reducer: pure function (state, action) => newState
type Action = { type: "inc" } | { type: "dec" } | { type: "reset" };
const reducer = (state: { count: number }, action: Action) => {
  switch (action.type) {
    case "inc":   return { count: state.count + 1 };
    case "dec":   return { count: state.count - 1 };
    case "reset": return { count: 0 };
  }
};

// 3. useMemo: memoize pure computation
const result = useMemo(() => expensiveCompute(data), [data]);

// 4. Immer: mutable API → immutable result
import { produce } from "immer";
const next = produce(state, (draft) => {
  draft.todos.push(newTodo); // looks mutable, produces new object
});

16. Common Pitfalls & Best Practices

Common Mistakes
  • Side effects inside map/filter callbacks
  • Forgetting Object.freeze is shallow
  • Over-currying hurting readability
  • Unbounded recursion causing stack overflow
  • Abusing point-free style making code cryptic
  • Using reduce over for in performance-critical loops
Best Practices
  • Prefer pipe over nested function calls
  • Use TypeScript strict mode for type safety
  • Small functions + composition > large functions
  • Model business logic with ADTs, make illegal states unrepresentable
  • Adopt FP incrementally in team projects, avoid full rewrites
  • Write property-based tests for pure functions

Conclusion

Functional programming is not just a programming paradigm — it is a way of thinking. It teaches us how to build reliable, maintainable software through pure functions, immutable data, and composable abstractions. You do not need to switch to Haskell to enjoy FP benefits — TypeScript with fp-ts or Effect-TS already lets you apply most FP concepts in daily development. Start with pure functions and immutability, gradually introduce Maybe/Either for error handling, build data pipelines with pipe and compose, and you will find your code becomes more predictable, testable, and composable.

Frequently Asked Questions

What is a pure function and why does it matter?

A pure function always returns the same output for the same input and produces no side effects — no mutation of external state, no I/O, no randomness. Pure functions are easier to test, reason about, memoize, and parallelize. They form the foundation of functional programming and enable referential transparency, meaning any expression can be replaced with its value without changing program behavior.

What is immutability and how do I achieve it in JavaScript/TypeScript?

Immutability means data cannot be changed after creation. In JavaScript, use Object.freeze for shallow immutability, spread operators or structuredClone for copies, and const for bindings. In TypeScript, use Readonly<T>, ReadonlyArray<T>, and as const assertions. Libraries like Immer provide convenient immutable updates with a mutable API through structural sharing.

What is the difference between currying and partial application?

Currying transforms a function that takes multiple arguments into a chain of single-argument functions: f(a, b, c) becomes f(a)(b)(c). Partial application fixes some arguments and returns a function expecting the remaining ones: partial(f, a) returns g(b, c). Currying always produces unary functions, while partial application can produce functions of any arity. Haskell curries all functions by default.

What is a Monad and why should developers learn about it?

A Monad is a design pattern for composing computations that produce wrapped values. It must implement two operations: unit (wrapping a value) and flatMap/bind (chaining computations on wrapped values). Common monads include Maybe/Option (handling nulls), Either/Result (error handling), Promise (async operations), and List (non-determinism). Understanding monads helps manage side effects, error propagation, and async flows in a composable way.

How does function composition work and what are its benefits?

Function composition combines two or more functions to create a new function where the output of one becomes the input of the next: compose(f, g)(x) = f(g(x)). Pipe works left-to-right: pipe(f, g)(x) = g(f(x)). Benefits include code reuse, readability (describing what rather than how), testability (each piece is independently testable), and the ability to build complex transformations from simple building blocks.

What are algebraic data types (ADTs) and how are they used?

Algebraic data types (ADTs) combine types using sum types (OR — tagged unions/discriminated unions) and product types (AND — tuples/records). Sum types like type Shape = Circle | Rectangle model exclusive choices. Product types like { radius: number; color: string } combine multiple fields. ADTs enable exhaustive pattern matching, making illegal states unrepresentable. TypeScript supports ADTs via discriminated unions with a tag field.

What is pattern matching and how does it compare across languages?

Pattern matching is a mechanism for checking a value against patterns and destructuring data. Haskell and Scala have built-in pattern matching with exhaustiveness checking. TypeScript achieves similar results using switch on discriminated unions with the never type for exhaustiveness. The TC39 pattern matching proposal (Stage 1) aims to bring native pattern matching to JavaScript with a match expression syntax.

How does Haskell compare to TypeScript for functional programming?

Haskell is a purely functional language with lazy evaluation, algebraic data types, type classes, and monadic I/O by default. TypeScript is a multi-paradigm language that supports functional patterns through libraries and conventions but does not enforce purity or immutability. Haskell has superior type inference (Hindley-Milner), pattern matching, and function composition syntax. TypeScript has a larger ecosystem, lower learning curve, and better industry adoption for web development.

𝕏 Twitterin LinkedIn
Czy to było pomocne?

Bądź na bieżąco

Otrzymuj cotygodniowe porady i nowe narzędzia.

Bez spamu. Zrezygnuj kiedy chcesz.

Try These Related Tools

{ }JSON FormatterJSTypeScript to JavaScript

Related Articles

TypeScript Type Guards: Kompletny Przewodnik Sprawdzania Typów

Opanuj type guards TypeScript: typeof, instanceof, in i niestandardowe guardy.

Software Design Patterns Guide: Creational, Structural & Behavioral Patterns

Comprehensive design patterns guide covering Factory, Builder, Singleton, Adapter, Decorator, Proxy, Facade, Observer, Strategy, Command, and State patterns with TypeScript and Python examples.

Data Structures and Algorithms Guide: Arrays, Trees, Graphs, Hash Tables & Big O

Complete data structures and algorithms guide for developers. Learn arrays, linked lists, trees, graphs, hash tables, heaps, stacks, queues, Big O notation, sorting algorithms, and searching with practical code examples in TypeScript and Python.