Polyglot
Languages TypeScript functional
TypeScript § functional

Functional patterns

TypeScript admits substantial functional programming through JavaScript’s first-class functions and TypeScript’s type-level expressiveness: arrow functions, closures, higher-order functions, immutability (via readonly and as const), function composition, currying, and the array methods (map, filter, reduce, flatMap). The Promise and async iterator protocols admit substantial functional handling of asynchronous data. The conventional discipline favours immutable updates, pure functions where possible, and method-chaining transformations on arrays. Substantial functional libraries (fp-ts, Ramda, lodash/fp) admit a more elaborate functional style; the conventional contemporary code uses the built-in array methods plus careful immutability.

First-class functions

Functions are values:

const add = (a: number, b: number): number => a + b;

const operation: (a: number, b: number) => number = add;
const result = operation(2, 3);                  // 5

// Stored in arrays:
const operations: Array<(a: number, b: number) => number> = [
    (a, b) => a + b,
    (a, b) => a - b,
    (a, b) => a * b,
];

// Returned from functions:
function makeAdder(n: number): (x: number) => number {
    return x => x + n;
}

const add5 = makeAdder(5);
console.log(add5(3));                            // 8

The conventional contemporary uses are callbacks, higher-order functions, and event handlers.

Higher-order functions

Functions that take or return other functions:

function applyTwice<T>(fn: (x: T) => T, x: T): T {
    return fn(fn(x));
}

const result = applyTwice(n => n + 1, 5);        // 7

// Returning a function:
function withLogging<A extends unknown[], R>(
    fn: (...args: A) => R,
    label: string,
): (...args: A) => R {
    return (...args: A) => {
        console.log(`[${label}] called with`, args);
        const result = fn(...args);
        console.log(`[${label}] returned`, result);
        return result;
    };
}

const loggedAdd = withLogging((a: number, b: number) => a + b, "add");
loggedAdd(2, 3);

Closures

Functions admit capturing variables from the enclosing scope:

function makeCounter(): { increment(): number; decrement(): number; get(): number } {
    let count = 0;
    return {
        increment: () => ++count,
        decrement: () => --count,
        get: () => count,
    };
}

const c = makeCounter();
c.increment();                                    // 1
c.increment();                                    // 2
c.get();                                          // 2

The closure admits state encapsulation without classes; the pattern is conventional for module-scope state and small abstractions.

Function composition

Composing two functions:

function compose<A, B, C>(f: (a: A) => B, g: (b: B) => C): (a: A) => C {
    return a => g(f(a));
}

const inc = (n: number) => n + 1;
const double = (n: number) => n * 2;
const incThenDouble = compose(inc, double);

console.log(incThenDouble(5));                   // (5+1)*2 = 12

For multi-argument composition, varidic forms admit chaining:

function pipe<A>(value: A): A;
function pipe<A, B>(value: A, ab: (a: A) => B): B;
function pipe<A, B, C>(value: A, ab: (a: A) => B, bc: (b: B) => C): C;
function pipe<A, B, C, D>(value: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D;
function pipe(value: any, ...fns: Array<(x: any) => any>): any {
    return fns.reduce((acc, fn) => fn(acc), value);
}

const result = pipe(
    5,
    n => n + 1,
    n => n * 2,
    n => n.toString(),
);
// result: "12"

Libraries like fp-ts provide substantial pipe and compose utilities; the native syntax is the pipe operator (|>), still a TC39 proposal as of 2026.

Currying

Transforming an n-argument function into a chain of single-argument functions:

function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
    return a => b => fn(a, b);
}

const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
const add5 = curriedAdd(5);
console.log(add5(3));                            // 8

For multi-argument currying, libraries (Ramda, fp-ts) admit substantial currying with type inference. The conventional TypeScript code uses partial application via closures rather than full currying.

Immutability

The conventional functional discipline favours immutability:

readonly properties

interface Point {
    readonly x: number;
    readonly y: number;
}

const p: Point = { x: 1, y: 2 };
// p.x = 10;                                       // ERROR: readonly

// Immutable update:
const moved: Point = { ...p, x: 10 };

readonly arrays

const arr: readonly number[] = [1, 2, 3];
// arr.push(4);                                    // ERROR
// arr[0] = 0;                                      // ERROR

const doubled = arr.map(x => x * 2);             // OK; produces a new array

as const for deep readonly

const config = {
    server: { host: "localhost", port: 8080 },
    features: ["a", "b", "c"],
} as const;

// config.server.port = 9090;                      // ERROR: readonly
// config.features.push("d");                      // ERROR: readonly

The as const admits substantial immutability for configuration data.

Deep readonly utility

type DeepReadonly<T> = T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

interface Config {
    server: {
        host: string;
        port: number;
    };
}

const cfg: DeepReadonly<Config> = { server: { host: "x", port: 1 } };
// cfg.server.port = 2;                            // ERROR

Pure functions

A pure function depends only on its inputs and produces no side effects:

// Pure:
function add(a: number, b: number): number {
    return a + b;
}

function double(arr: readonly number[]): number[] {
    return arr.map(x => x * 2);
}

// Impure (mutates input):
function doubleInPlace(arr: number[]): void {
    for (let i = 0; i < arr.length; i++) {
        arr[i] *= 2;
    }
}

// Impure (logs to console):
function log(message: string): void {
    console.log(message);
}

The conventional discipline:

  • Default to pure functions — they are easier to test and reason about.
  • Mark mutation explicitly — through void-returning functions or method names.
  • Use readonly on parameters that are not modified.

Array methods

The principal functional-style transformations:

const items = [1, 2, 3, 4, 5];

// Transformation:
const doubled = items.map(x => x * 2);

// Filtering:
const evens = items.filter(x => x % 2 === 0);

// Aggregation:
const sum = items.reduce((acc, x) => acc + x, 0);
const max = items.reduce((m, x) => x > m ? x : m, -Infinity);

// Discrimination:
const hasEven = items.some(x => x % 2 === 0);
const allPositive = items.every(x => x > 0);

// Search:
const first = items.find(x => x > 3);
const idx = items.findIndex(x => x > 3);

// Flattening:
const nested = [[1, 2], [3], [4, 5, 6]];
const flat = nested.flat();                      // [1, 2, 3, 4, 5, 6]
const flatMapped = items.flatMap(x => [x, x * 2]);

// Combining:
const sorted = [...items].sort((a, b) => a - b);
const reversed = [...items].reverse();

Method chaining

const result = items
    .filter(x => x.active)
    .map(x => ({ id: x.id, name: x.name.toUpperCase() }))
    .sort((a, b) => a.name.localeCompare(b.name));

The form is conventional for substantial pipelines.

Tagged unions and pattern-matching style

While TypeScript lacks pattern matching, discriminated unions admit functional-style dispatch:

type Result<T, E> =
    | { ok: true; value: T }
    | { ok: false; error: E };

function map<T, U, E>(r: Result<T, E>, f: (value: T) => U): Result<U, E> {
    return r.ok ? { ok: true, value: f(r.value) } : r;
}

function flatMap<T, U, E>(r: Result<T, E>, f: (value: T) => Result<U, E>): Result<U, E> {
    return r.ok ? f(r.value) : r;
}

function getOrElse<T, E>(r: Result<T, E>, defaultValue: T): T {
    return r.ok ? r.value : defaultValue;
}

const result: Result<number, string> = { ok: true, value: 5 };
const doubled = map(result, n => n * 2);         // Result<number, string>
const value = getOrElse(doubled, 0);              // 10

The pattern admits substantial functional handling without exceptions; treated in Error handling.

Promise as a functor

Promises admit functional composition:

const userPromise: Promise<User> = fetchUser(id);

const namePromise = userPromise.then(u => u.name);
const greetingPromise = namePromise.then(n => `Hello, ${n}`);

// Chained:
const greeting = await fetchUser(id)
    .then(u => u.name)
    .then(n => `Hello, ${n}`);

Treated in Async and concurrency.

Common patterns

Predicate combinators

type Predicate<T> = (x: T) => boolean;

function and<T>(...preds: Predicate<T>[]): Predicate<T> {
    return x => preds.every(p => p(x));
}

function or<T>(...preds: Predicate<T>[]): Predicate<T> {
    return x => preds.some(p => p(x));
}

function not<T>(pred: Predicate<T>): Predicate<T> {
    return x => !pred(x);
}

const isPositive = (n: number) => n > 0;
const isEven = (n: number) => n % 2 === 0;
const isPositiveEven = and(isPositive, isEven);

Lazy evaluation via generators

function* take<T>(iter: Iterable<T>, n: number): Generator<T> {
    let i = 0;
    for (const x of iter) {
        if (i++ >= n) break;
        yield x;
    }
}

function* range(start = 0): Generator<number> {
    for (let i = start; ; i++) yield i;
}

const first10Squares = [...take(
    function* () {
        for (const n of range()) yield n * n;
    }(),
    10,
)];
// [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Functional-style state

type State = { count: number; history: number[] };

function increment(s: State): State {
    return {
        count: s.count + 1,
        history: [...s.history, s.count + 1],
    };
}

function decrement(s: State): State {
    return {
        count: s.count - 1,
        history: [...s.history, s.count - 1],
    };
}

const initial: State = { count: 0, history: [] };
const after = [increment, increment, decrement, increment]
    .reduce((s, f) => f(s), initial);
// after.count: 2; after.history: [1, 2, 1, 2]

Reducer pattern

type Action =
    | { type: "INCREMENT"; by: number }
    | { type: "DECREMENT"; by: number }
    | { type: "RESET" };

function reducer(state: number, action: Action): number {
    switch (action.type) {
        case "INCREMENT": return state + action.by;
        case "DECREMENT": return state - action.by;
        case "RESET":     return 0;
    }
}

const final = [
    { type: "INCREMENT", by: 5 },
    { type: "INCREMENT", by: 3 },
    { type: "DECREMENT", by: 2 },
] as const satisfies readonly Action[];

const result = final.reduce(reducer, 0);          // 6

Memoization

function memoize<A extends readonly unknown[], R>(fn: (...args: A) => R): (...args: A) => R {
    const cache = new Map<string, R>();
    return (...args: A) => {
        const key = JSON.stringify(args);
        if (!cache.has(key)) cache.set(key, fn(...args));
        return cache.get(key)!;
    };
}

const fib = memoize((n: number): number =>
    n < 2 ? n : fib(n - 1) + fib(n - 2));

Function pipelines

const slugify = (s: string): string =>
    s.toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-|-$/g, "");

const users = await fetchUsers();
const namesAndAges = users
    .filter(u => u.active)
    .map(u => ({ name: u.name, age: u.age }))
    .sort((a, b) => a.age - b.age);

Maybe-style helpers

function map<T, U>(value: T | null | undefined, fn: (x: T) => U): U | null | undefined {
    return value == null ? value : fn(value);
}

const length = map(getString(), s => s.length);

function getOrElse<T>(value: T | null | undefined, defaultValue: T): T {
    return value ?? defaultValue;
}

Side-effect isolation

async function process(input: string): Promise<Result> {
    const validated = validate(input);            // pure
    const parsed = parse(validated);              // pure
    const result = compute(parsed);               // pure

    await save(result);                           // side effect, isolated
    log(`Processed ${input}`);                   // side effect, isolated

    return result;
}

Tag-based union dispatch

type Event = { type: "click"; x: number; y: number }
           | { type: "key"; code: string };

const handlers = {
    click: (e: Extract<Event, { type: "click" }>) => `clicked at (${e.x}, ${e.y})`,
    key:   (e: Extract<Event, { type: "key" }>) => `pressed ${e.code}`,
} as const;

function handle(e: Event): string {
    // @ts-expect-error: dispatch by type
    return handlers[e.type](e);
}

A note on functional libraries

Substantial functional libraries admit elaborate functional patterns:

  • fp-ts — Haskell-style monads, Option, Either, Task, Reader, etc. Substantial type-level expressiveness.
  • Ramda — Heavy currying, point-free style.
  • lodash/fp — Functional variants of lodash utilities.
  • Immer — Immutable updates with imperative syntax.
  • zustand, Effect, Redux Toolkit — Functional state management.

The conventional contemporary TypeScript application uses the built-in array methods plus careful immutability; substantial libraries are reached for in domains where the additional rigor is genuinely valuable.

A note on the conventional discipline

The contemporary TypeScript functional advice:

  • Use arrow functions for callbacks and small functions.
  • Use array methods (map, filter, reduce) over imperative loops for transformations.
  • Use readonly on parameters and properties that are not modified.
  • Use spread (...) for immutable updates.
  • Use as const for literal-type configuration.
  • Use closures for state encapsulation.
  • Use higher-order functions for callbacks and combinators.
  • Use discriminated unions for sum types and pattern-style dispatch.
  • Use Promises for async composition.
  • Reach for fp-ts etc. only when substantive functional programming is genuinely valuable.

The combination — first-class functions, closures, higher-order functions, the array-method API, immutability via readonly and spread, discriminated unions — is the substance of TypeScript’s functional surface. The discipline produces clear, type-safe, composable code without the substantial overhead of more elaborate functional systems.