Introduction
Functional programming in JavaScript doesn't require full purity. Mix FP where it helps, use objects/classes where they fit better.
Key Concepts
Pragmatic FP: Using functional patterns where they help, without dogmatic purity.
Functional Core, Imperative Shell: Pure business logic surrounded by effectful I/O boundaries.
FP Libraries: Ramda (auto-curried, data-last), fp-ts (typed FP for TypeScript), Immer (immutable state).
Real World Context
Most production JavaScript is a mix of FP and OOP. React components are functional but use class-based error boundaries. Redux reducers are pure functions called by an imperative dispatch system. The goal is to use the right tool for each problem, not to be 100% functional.
Deep Dive
Using FP Libraries
javascript// Ramda - auto-curried, data-last import { pipe, map, filter, prop } from 'ramda'; const getActiveUserNames = pipe( filter(prop('active')), map(prop('name')) ); // fp-ts - TypeScript FP import { pipe } from 'fp-ts/function'; import { map, filter } from 'fp-ts/Array'; import * as O from 'fp-ts/Option';
Mixing Paradigms
javascriptclass UserService { constructor(api) { this.api = api; } // Methods use FP internally async getActiveUserNames() { const users = await this.api.fetchUsers(); return pipe( users, filter(u => u.active), map(u => u.name) ); } }
When to Use FP
javascript// Good for: Data transformation const processOrders = pipe( filter(order => order.status === 'complete'), map(order => ({ ...order, total: calcTotal(order) })), sortBy(prop('date')) ); // Consider classes for: Stateful services, complex domains class ShoppingCart { #items = []; add(item) { /* ... */ } remove(id) { /* ... */ } }
Common Pitfalls
- Forcing purity everywhere — I/O, DOM manipulation, and timers are inherently impure. Isolate them, don't eliminate them.
- Library dependency for simple patterns — You don't need Ramda for
arr.map(x => x.name). Use libraries when they add real value. - Readability trade-offs — A point-free pipeline with 8 composed functions is harder to maintain than explicit code. Optimize for the team's understanding.
Best Practices
- Use FP for data transformation —
pipe(filter, map, reduce)is cleaner than nested loops for data processing. - Use classes for stateful services — Database connections, WebSocket managers, and caches are naturally stateful. Use classes.
- Start small — Introduce FP patterns incrementally: pure functions first, then composition, then functors/monads as needed.
Summary
Use FP for data transformations. Mix with OOP where appropriate. Libraries like Ramda, fp-ts help. Don't force purity everywhere.
Code Examples
import { pipe, filter, map, prop } from 'ramda';
// FP for data transformation
const getActiveUserNames = pipe(
filter(prop('active')),
map(prop('name'))
);
// OOP for stateful services
class UserService {
constructor(api) { this.api = api; }
async getActiveNames() {
const users = await this.api.fetchUsers(); // Imperative shell
return getActiveUserNames(users); // Functional core
}
}
// Mix paradigms pragmatically
const service = new UserService(apiClient);
const names = await service.getActiveNames();