Functional Programming in TypeScript: from Theory to Practice
Functional Programming (FP) is a paradigm in which computation is treated as the evaluation of mathematical functions and avoids changing state and mutable data. TypeScript, being a superset of JavaScript, is well-suited for applying FP principles due to its strict typing and support for higher-order functions. In this article, we will break down the key concepts of FP in TypeScript and show how to apply them in practice.
1. Pure Functions and Immutability
A pure function is a function that:
- Always returns the same result for the same arguments (deterministic).
- Has no side effects (does not modify external variables, does not output data, does not make requests).
Example of a pure function:
// Pure function: sums two numbersconst add = (a: number, b: number): number => a + b;
// Always returns 5console.log(add(2, 3)); // 5console.log(add(2, 3)); // 5Immutability is the principle where data is not changed after creation. Instead of modifying an existing object, a new one is created.
// Immutable array updateconst numbers: number[] = [1, 2, 3];
// Bad (mutation):numbers.push(4);
// Good (immutable):const newNumbers = [...numbers, 4]; // [1,2,3,4]
// Immutable object updateinterface User { name: string; age: number;}
const user: User = { name: 'Alice', age: 30 };const updatedUser: User = { ...user, age: 31 }; // new object2. Function Composition and Currying
Function composition is combining two or more functions into a new one. The result of the first function is passed as an argument to the second.
// Composition of two functionsconst compose = <T, U, V>(f: (x: U) => V, g: (x: T) => U) => (x: T): V => f(g(x));
const double = (x: number): number => x * 2;const increment = (x: number): number => x + 1;
const doubleThenIncrement = compose(increment, double);console.log(doubleThenIncrement(3)); // (3*2)+1 = 7Currying is transforming a function with multiple arguments into a sequence of functions with a single argument.
// Regular functionconst multiply = (a: number, b: number): number => a * b;
// Curried versionconst curriedMultiply = (a: number) => (b: number): number => a * b;
const multiplyBy2 = curriedMultiply(2);console.log(multiplyBy2(5)); // 10console.log(multiplyBy2(7)); // 14Currying simplifies partial application and function reuse.
3. Functors and Monads: Maybe and Either
A functor is a container that implements the map method, allowing a function to be applied to the value inside the container. In TypeScript, an array is an example of a functor:
const numbers = [1, 2, 3];const doubled = numbers.map(x => x * 2); // [2, 4, 6]A monad is a functor that additionally implements the flatMap (or bind) method and of (or return). Monads help handle side effects, null values, and errors without exceptions.
Let's implement a simple Maybe monad for safe handling of null/undefined:
// Maybe monadclass Maybe<T> { private constructor(private value: T | null) {}
static of<T>(value: T): Maybe<T> { return new Maybe(value); }
static none<T>(): Maybe<T> { return new Maybe<T>(null); }
map<U>(fn: (value: T) => U): Maybe<U> { if (this.value === null || this.value === undefined) { return Maybe.none(); } return M