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
readonlyon 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
readonlyon parameters and properties that are not modified. - Use spread (
...) for immutable updates. - Use
as constfor 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-tsetc. 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.