17.1 Functional Programming Patterns — pipe/compose and Option/Result Types
Core Functional Programming Concepts
In TypeScript, functional programming creates predictable, testable code through immutability, pure functions, and function composition.
// Pure function — same input, same output
const double = (n: number): number => n * 2
const addOne = (n: number): number => n + 1
// Impure function — side effects
let count = 0
const increment = () => ++count // Mutates external state
pipe and compose
// pipe — executes left to right
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C
function pipe<A, B, C, D>(
fn1: (a: A) => B,
fn2: (b: B) => C,
fn3: (c: C) => D
): (a: A) => D
function pipe(...fns: Array<(arg: unknown) => unknown>) {
return (value: unknown) => fns.reduce((acc, fn) => fn(acc), value)
}
// compose — executes right to left
function compose<A, B, C>(
fn2: (b: B) => C,
fn1: (a: A) => B
): (a: A) => C {
return (value: A) => fn2(fn1(value))
}
// Usage example
const processNumber = pipe(
(n: number) => n * 2, // double
(n: number) => n + 1, // add one
(n: number) => `Result: ${n}` // to string
)
console.log(processNumber(5)) // "Result: 11"
// String processing pipeline
const processName = pipe(
(s: string) => s.trim(),
(s: string) => s.toLowerCase(),
(s: string) => s.replace(/\s+/g, '-')
)
console.log(processName(' Hello World ')) // "hello-world"
Option Type (Maybe Monad)
// Type for safely handling null/undefined
type None = { readonly _tag: 'None' }
type Some<A> = { readonly _tag: 'Some'; readonly value: A }
type Option<A> = None | Some<A>
// Constructors
const none: None = { _tag: 'None' }
const some = <A>(value: A): Some<A> => ({ _tag: 'Some', value })
// Pattern matching
function fold<A, B>(
onNone: () => B,
onSome: (value: A) => B
): (option: Option<A>) => B {
return (option) => {
if (option._tag === 'None') return onNone()
return onSome(option.value)
}
}
// map — transform only when value exists
function mapOption<A, B>(fn: (a: A) => B) {
return (option: Option<A>): Option<B> => {
if (option._tag === 'None') return none
return some(fn(option.value))
}
}
// flatMap — chain with functions that return Option
function flatMapOption<A, B>(fn: (a: A) => Option<B>) {
return (option: Option<A>): Option<B> => {
if (option._tag === 'None') return none
return fn(option.value)
}
}
// Real-world usage
function findUser(id: string): Option<User> {
const user = users.find(u => u.id === id)
return user ? some(user) : none
}
function getEmail(user: User): Option<string> {
return user.email ? some(user.email) : none
}
// Safe chaining
const getEmailById = (id: string): Option<string> =>
pipe(
findUser(id),
flatMapOption(getEmail)
)
const result = getEmailById('user-1')
const email = fold(
() => 'No email',
(e) => e
)(result)
Result Type (Either Monad)
// Express success/failure as types
type Ok<T> = { readonly _tag: 'Ok'; readonly value: T }
type Err<E> = { readonly _tag: 'Err'; readonly error: E }
type Result<T, E = Error> = Ok<T> | Err<E>
// Constructors
const ok = <T>(value: T): Ok<T> => ({ _tag: 'Ok', value })
const err = <E>(error: E): Err<E> => ({ _tag: 'Err', error })
// Utilities
function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
return result._tag === 'Ok'
}
function mapResult<T, U, E>(fn: (value: T) => U) {
return (result: Result<T, E>): Result<U, E> => {
if (result._tag === 'Err') return result
return ok(fn(result.value))
}
}
function flatMapResult<T, U, E>(fn: (value: T) => Result<U, E>) {
return (result: Result<T, E>): Result<U, E> => {
if (result._tag === 'Err') return result
return fn(result.value)
}
}
// Wrap error-throwing functions in Result
function tryCatch<T>(fn: () => T): Result<T, Error> {
try {
return ok(fn())
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)))
}
}
// Async version
async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
try {
return ok(await fn())
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)))
}
}
// Real-world usage — error handling pipeline
async function processUserRegistration(data: unknown): Promise<Result<User, string>> {
return pipe(
validateInput(data), // Result<ValidInput, string>
flatMapResult(hashPassword), // Result<HashedInput, string>
flatMapResult(createUser), // Result<User, string>
)
}
const result = await processUserRegistration(userData)
if (isOk(result)) {
console.log('User created:', result.value)
} else {
console.error('Error:', result.error)
}
Pro Tips
fp-ts Library
npm install fp-ts
import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import * as E from 'fp-ts/Either'
import * as A from 'fp-ts/Array'
import * as TE from 'fp-ts/TaskEither' // Async Either
// Option chaining
const result = pipe(
O.fromNullable(users.find(u => u.id === '1')),
O.map(user => user.email),
O.filter(Boolean),
O.getOrElse(() => 'default@example.com')
)
// TaskEither — async error handling
const fetchUserSafe = (id: string): TE.TaskEither<Error, User> =>
TE.tryCatch(
() => fetch(`/api/users/${id}`).then(r => r.json()),
(e) => e instanceof Error ? e : new Error(String(e))
)
const program = pipe(
fetchUserSafe('user-1'),
TE.map(user => user.email),
TE.fold(
(error) => async () => console.error(error.message),
(email) => async () => console.log(email)
)
)
await program()