Functional programming principles reduce bugs by eliminating implicit state and making data flow explicit. Claude Code applies FP correctly: generating pure functions that are trivially testable, using composition to build complex behavior from simple pieces, and implementing the Either/Option pattern for explicit error handling without exceptions.
This guide covers functional programming with Claude Code: pure functions, composition, fp-ts monads, and practical patterns.
Pure Functions
My order calculation function has a bug but it's hard to test
because it reads from a database and modifies global state.
Make it pure.
// Before: impure — depends on external state, has side effects
async function calculateOrderTotal(orderId: string): Promise<void> {
const order = await db.orders.findOne(orderId); // External I/O
currentOrders.push(order); // Mutates global state
let total = 0;
for (const item of order.items) {
const product = await db.products.findOne(item.productId); // Another I/O
total += product.price * item.quantity;
}
order.total = total; // Mutations
await db.orders.update(orderId, { total }); // Side effect
console.log(`Updated order ${orderId}`); // Side effect
}
// After: pure core logic + explicit I/O at the edges
// Pure: no I/O, no mutation, no side effects — trivially testable
function calculateLineItemTotal(priceCents: number, quantity: number): number {
return priceCents * quantity;
}
function calculateOrderSubtotal(items: Array<{ priceCents: number; quantity: number }>): number {
return items.reduce((sum, item) => sum + calculateLineItemTotal(item.priceCents, item.quantity), 0);
}
function applyDiscount(subtotalCents: number, discountPercent: number): number {
return Math.floor(subtotalCents * (1 - discountPercent / 100));
}
// I/O at the edge — impure wrapper that uses pure core
async function updateOrderTotal(orderId: string, db: Database): Promise<Order> {
// Gather all data needed
const order = await db.orders.findWithItems(orderId); // I/O here
// Pure calculation
const subtotal = calculateOrderSubtotal(order.items);
const total = order.discountPercent
? applyDiscount(subtotal, order.discountPercent)
: subtotal;
// Update
return db.orders.update(orderId, { totalCents: total }); // I/O here
}
Function Composition
I need to process incoming order data through a pipeline:
validate → normalize → enrich → transform.
Use composition instead of nested function calls.
// Without composition — hard to read, hard to modify order
function processOrder(raw: RawOrder): ProcessedOrder {
return transformForStorage(enrichWithPricing(normalizeFields(validateOrder(raw))));
}
// With pipe composition — reads left to right
import { pipe } from 'fp-ts/function';
const processOrder = (raw: RawOrder): ProcessedOrder =>
pipe(
raw,
validateOrder,
normalizeFields,
enrichWithPricing,
transformForStorage,
);
// Each function is independently testable:
const validateOrder = (raw: RawOrder): ValidatedOrder => {
if (!raw.userId) throw new Error('Missing userId');
if (!raw.items?.length) throw new Error('Empty order');
return raw as ValidatedOrder;
};
const normalizeFields = (order: ValidatedOrder): NormalizedOrder => ({
...order,
userId: order.userId.toLowerCase().trim(),
items: order.items.map(i => ({ ...i, productId: i.productId.trim() })),
});
// Compose async functions with pipeWith
const pipeAsync = <A, B>(fn: (a: A) => Promise<B>) => async (a: A): Promise<B> => fn(a);
// For async: use fp-ts TaskEither or manual promise chaining
const processOrderAsync = (raw: RawOrder): Promise<ProcessedOrder> =>
Promise.resolve(raw)
.then(validateOrder)
.then(normalizeFields)
.then(enrichWithPricingAsync)
.then(transformForStorage);
Either Monad for Error Handling
My API route has try/catch everywhere with inconsistent error handling.
Use Either to make error paths explicit without exceptions.
// fp-ts Either: Left = error, Right = success
import { Either, left, right, isLeft, map, flatMap, fold } from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
// Domain errors as types (not exceptions)
type OrderError =
| { type: 'USER_NOT_FOUND'; userId: string }
| { type: 'INSUFFICIENT_STOCK'; productId: string; available: number; requested: number }
| { type: 'PAYMENT_FAILED'; reason: string };
// Functions return Either instead of throwing
function validateUser(userId: string, users: User[]): Either<OrderError, User> {
const user = users.find(u => u.id === userId);
return user
? right(user)
: left({ type: 'USER_NOT_FOUND', userId });
}
function checkStock(
items: OrderItem[],
inventory: Map<string, number>,
): Either<OrderError, OrderItem[]> {
for (const item of items) {
const available = inventory.get(item.productId) ?? 0;
if (available < item.quantity) {
return left({
type: 'INSUFFICIENT_STOCK',
productId: item.productId,
available,
requested: item.quantity,
});
}
}
return right(items);
}
// Chain operations — short-circuits on first Left
function createOrder(
userId: string,
items: OrderItem[],
users: User[],
inventory: Map<string, number>,
): Either<OrderError, Order> {
return pipe(
validateUser(userId, users),
flatMap(user =>
pipe(
checkStock(items, inventory),
map(validItems => ({ userId: user.id, items: validItems, status: 'pending' as const })),
),
),
);
}
// At the edge: convert Either to HTTP response
function handleCreateOrder(req: Request, res: Response) {
const result = createOrder(
req.body.userId,
req.body.items,
users,
inventory,
);
pipe(
result,
fold(
// Left: error case
(error) => {
switch (error.type) {
case 'USER_NOT_FOUND':
return res.status(404).json({ error: `User ${error.userId} not found` });
case 'INSUFFICIENT_STOCK':
return res.status(409).json({
error: `Insufficient stock`,
available: error.available,
requested: error.requested,
});
case 'PAYMENT_FAILED':
return res.status(402).json({ error: error.reason });
}
},
// Right: success case
(order) => res.status(201).json(order),
),
);
}
Option for Nullable Values
// Option<A> = None | Some<A> — explicit nullable without null checks
import { Option, none, some, map, getOrElse, fromNullable } from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';
// Functions that may not return a value
function findUser(id: string): Option<User> {
const user = users.find(u => u.id === id);
return fromNullable(user); // Converts undefined/null to None
}
function getPrimaryEmail(user: User): Option<string> {
return fromNullable(user.emails.find(e => e.primary)?.address);
}
// Chain optional operations
function getUserPrimaryEmail(userId: string): Option<string> {
return pipe(
findUser(userId),
flatMap(getPrimaryEmail), // If user not found, stays None
);
}
// Extract value with default
const email = pipe(
getUserPrimaryEmail('user-1'),
getOrElse(() => '[email protected]'),
);
Practical FP Without fp-ts
I want FP principles but don't want the fp-ts dependency.
Show me practical immutable patterns in plain TypeScript.
// Immutable data updates without fp-ts
// Using object spread for updates (no mutation)
function updateUserEmail(user: User, newEmail: string): User {
return { ...user, email: newEmail }; // Returns new object, original unchanged
}
// Immutable array operations
function addToCart(cart: CartItem[], item: CartItem): CartItem[] {
const existing = cart.findIndex(i => i.productId === item.productId);
if (existing >= 0) {
// Update quantity without mutation
return cart.map((c, i) =>
i === existing ? { ...c, quantity: c.quantity + item.quantity } : c,
);
}
return [...cart, item]; // New array with added item
}
function removeFromCart(cart: CartItem[], productId: string): CartItem[] {
return cart.filter(i => i.productId !== productId);
}
// Result type — no fp-ts required
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function safeDivide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: 'Division by zero' };
return { ok: true, value: a / b };
}
// Chain results
function chainResult<T, U, E>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>,
): Result<U, E> {
return result.ok ? fn(result.value) : result;
}
For the TypeScript type system that makes FP patterns more powerful (branded types, discriminated unions), see the advanced TypeScript guide. For state machines as a structured alternative to deeply nested conditionals, see the state machines guide. The Claude Skills 360 bundle includes functional programming skill sets covering composition patterns, Either/Option types, and practical FP in TypeScript. Start with the free tier to try functional programming code generation.