Functional Programming in TypeScript: From A to Z
Functional programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. TypeScript, being a strictly typed extension of JavaScript, provides an excellent foundation for applying FP principles, allowing you to write more predictable, testable, and reliable code. In this article, we will break down the key concepts of FP in TypeScript, starting from the basics and moving to advanced techniques.
Basic Principles of Functional Programming
Before diving into code, it is important to understand the fundamental principles on which FP is built.
Pure Functions
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 to the console, does not send requests).
Example of a pure function:
function add(a: number, b: number): number { return a + b;}
// Always predictableconsole.log(add(2, 3)); // 5console.log(add(2, 3)); // 5Example of an impure function:
let counter = 0;function increment(): number { return ++counter; // Side effect: modifying an external variable}Immutability
Data should not be changed after creation. Instead of modifying an existing object, we create a new copy with the necessary changes.
interface User { name: string; age: number;}
const user: User = { name: "Alice", age: 30 };
// Bad (mutation)user.age = 31;
// Good (immutability)const updatedUser: User = { ...user, age: 31 };
console.log(user); // { name: "Alice", age: 30 } - original unchangedconsole.log(updatedUser); // { name: "Alice", age: 31 }Composition and Currying in TypeScript
Function composition is the process of combining several simple functions to create a more complex one. Currying is the transformation of a function with multiple arguments into a sequence of functions, each taking a single argument.
Function Composition
A typed implementation of composition in TypeScript:
function compose<A, B, C>(f: (x: B) => C, g: (x: A) => B): (x: A) => C { return (x: A) => f(g(x));}
const toUpperCase = (str: string): string => str.toUpperCase();const exclaim = (str: string): string => `${str}!`;
const shout = compose(exclaim, toUpperCase);console.log(shout("hello world")); // "HELLO WORLD!"Currying
Example of a curried function:
function multiply(a: number): (b: number) => number { return (b: number) => a * b;}
const double = multiply(2);const triple = multiply(3);
console.log(double(5)); // 10console.log(triple(5)); // 15Currying makes functions more flexible and convenient for composition.
Working with Monads: Option and Result
Monads are a powerful pattern for handling side effects and error handling without breaking function purity. In TypeScript, the Option (or Maybe) and Result (or Either) monads are often used.
Option (Maybe) Monad
Allows safe work with values that may be null or undefined.
type Option<T> = T | null;
function safeDivide(a: number, b: number): Option<number> { return b === 0 ? null : a / b;}
function getValue(value: Option<number>): string { if (value === null) { return "Error: division by zero"; } return `Result: ${value}`;}
console.log(getValue(safeDivide(10, 2)