| name | functional |
| description | Functional programming patterns with immutable data. Use when writing logic, data transformations, or encountering mutation bugs. Covers immutability violations catalog, pure functions, composition, early returns, and options objects. Do NOT over-apply heavy FP abstractions (monads, fp-ts) unless the project requires them. |
Functional Patterns
Deep-dive resources are in the resources/ directory. Load them on demand:
| Resource | Load when... |
|---|
immutability-catalog.md | Fixing mutation bugs, applying readonly/ReadonlyArray types, or looking up the immutable alternative to an array/object mutation |
composition-patterns.md | Composing small functions into pipelines, refactoring monolithic logic, or flattening deeply nested code |
Core Principles
- No data mutation - immutable structures only
- Pure functions wherever possible
- Composition over inheritance
- No comments - code should be self-documenting
- Array methods over loops
- Options objects over positional parameters
Why Immutability Matters
Immutable data is the foundation of functional programming. It makes code predictable (same input → same output, no hidden state changes), debuggable (state never changes underneath you), testable (no hidden mutable state), React-friendly (reconciliation and memoization work correctly), and concurrency-safe (no race conditions).
const user = { name: 'Alice', permissions: ['read'] };
grantPermission(user, 'write');
console.log(user.permissions);
const updatedUser = grantPermission(user, 'write');
console.log(user.permissions);
console.log(updatedUser.permissions);
Use readonly on all data structure properties and ReadonlyArray<T> for arrays so the compiler enforces this. For the full catalog of mutations and their immutable alternatives, load resources/immutability-catalog.md.
Functional Light
Follow "Functional Light" principles - practical functional patterns without heavy abstractions:
- DO: pure functions, immutable data, composition, declarative code, array methods,
readonly type safety
- DON'T: category theory, monads, heavy FP libraries (fp-ts, Ramda), over-engineering, functional for its own sake
Why: The goal is maintainable, testable code - not academic purity. If a functional pattern makes code harder to understand, don't use it.
const activeUsers = users.filter(u => u.active);
const userNames = activeUsers.map(u => u.name);
const compose = <T>(...fns: Array<(arg: T) => T>) => (x: T) =>
fns.reduceRight((v, f) => f(v), x);
const activeUsers = compose(
filter((u: User) => u.active),
map((u: User) => u.name)
)(users);
No Comments / Self-Documenting Code
Code should be clear through naming and structure. Comments indicate unclear code.
Exceptions:
- JSDoc for public APIs when generating documentation
- "Why"-comments required by other skills: characterisation test file headers and SUSPICIOUS behavior markers (see the
characterisation-tests skill)
- Constraints the code cannot express (e.g. a workaround pinned to an upstream bug, an ordering requirement imposed by an external system)
❌ WRONG - Comments explaining unclear code
function check(u: any) {
if (u) {
if (u.a) {
if (u.p) return true;
}
}
return false;
}
✅ CORRECT - Self-documenting code
function canUserAccessResource(user: User | undefined): boolean {
if (!user) return false;
if (!user.isActive) return false;
if (!user.hasPermission) return false;
return true;
}
function canUserAccessResource(user: User | undefined): boolean {
return user !== undefined && user.isActive && user.hasPermission;
}
Check undefined explicitly in the boolean form: optional chaining (user?.isActive && user?.hasPermission) yields boolean | undefined and fails to compile under strict mode.
If code requires comments to understand, refactor instead: extract functions with descriptive names, use meaningful variable names, break complex logic into steps, use type aliases for domain concepts.
✅ Acceptable JSDoc for public APIs
export function registerScenario(definition: ScenaristScenario): void {
Array Methods Over Loops
Prefer map, filter, reduce for transformations. They're declarative (what, not how) and naturally immutable.
✅ CORRECT - map, filter, reduce, and chaining
const scenarioIds = scenarios.map(s => s.id);
const activeScenarios = scenarios.filter(s => s.active);
const total = items
.filter(item => item.active)
.map(item => item.price * item.quantity)
.reduce((sum, price) => sum + price, 0);
When Loops Are Acceptable
Imperative loops are fine when:
- Early termination is essential (use
for...of with break)
- Performance critical (measure first!)
- Side effects are necessary (logging, DOM manipulation)
But even then, prefer Array.find() for early termination and Array.some() / Array.every() for boolean checks.
Options Objects Over Positional Parameters
Default to options objects for function parameters: named parameters, no ordering dependencies, easy optional parameters, self-documenting call sites, TypeScript autocomplete.
✅ CORRECT - Options object
type CreatePaymentOptions = {
amount: number;
currency: string;
cardId: string;
cvv: string;
saveCard?: boolean;
sendReceipt?: boolean;
};
function createPayment(options: CreatePaymentOptions): Payment {
const { amount, currency, cardId, cvv, saveCard = false, sendReceipt = true } = options;
}
createPayment({ amount: 100, currency: 'GBP', cardId: 'card_123', cvv: '123', saveCard: true });
Use positional parameters only when: 1-2 parameters max, the order is obvious (e.g., add(a, b)), or for high-frequency utility functions.
Pure Functions
Pure functions have no side effects and always return the same output for the same input:
- No side effects - doesn't mutate external state, modify arguments, or perform I/O
- Deterministic - same input → same output; no dependency on
Date.now(), Math.random(), or globals
- Referentially transparent - can replace the call with its return value
Pure functions are testable (no setup/teardown), composable, predictable, cacheable, and parallelizable.
When Impurity Is Necessary
Some functions must be impure (I/O, randomness, side effects). Isolate them:
function calculateTotal(items: ReadonlyArray<Item>): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
async function saveOrder(order: Order): Promise<void> {
const total = calculateTotal(order.items);
await database.save({ ...order, total });
}
Pattern: Keep impure functions at system boundaries (adapters, ports). Keep core domain logic pure.
Early Returns Over Nesting
Max 2 levels of function nesting. Beyond that, extract functions or flatten with guard clauses. For worked flattening and composition examples, load resources/composition-patterns.md.
if (user) {
if (user.isActive) {
if (user.hasPermission) {
}
}
}
if (!user) return;
if (!user.isActive) return;
if (!user.hasPermission) return;
Result Type for Error Handling
type Result<T, E = Error> =
| { readonly success: true; readonly data: T }
| { readonly success: false; readonly error: E };
function processPayment(payment: Payment): Result<Transaction> {
if (payment.amount <= 0) {
return { success: false, error: new Error('Invalid amount') };
}
const transaction = executePayment(payment);
return { success: true, data: transaction };
}
const result = processPayment(payment);
if (!result.success) return logError(result.error);
console.log(result.data.transactionId);
Summary Checklist
When writing functional code, verify: