SOLID Principles in JavaScript: Practical Guide with TypeScript and React
Learn how SOLID principles apply to modern JavaScript development. Practical examples with TypeScript, React hooks, and functional patterns - plus when to use them and when they're overkill.
SOLID principles were formulated for object-oriented programming, but modern JavaScript development looks different - functional patterns, React hooks, dynamic typing. Do these principles still matter? The answer is yes, but with important adaptations.
These principles remain valuable in JavaScript, but they need translation. The challenge isn't whether to use them, but how to apply them in a language that favors composition over inheritance and duck typing over rigid interfaces.
Abstract
This post examines how SOLID's five principles - Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion - adapt to JavaScript and TypeScript development. Through practical examples with React, Node.js, and TypeScript, we'll explore when these principles improve code quality and when they lead to over-engineering. Each principle includes working code examples, anti-patterns to avoid, and specific guidance for JavaScript's functional and dynamic nature.
Understanding SOLID in JavaScript Context
The Challenge
SOLID principles were designed for statically-typed, class-based languages like Java and C#. JavaScript brings different characteristics:
- Dynamic typing - No compile-time type checking without TypeScript
- Functional patterns - Functions as first-class citizens
- Composition focus - Less emphasis on inheritance
- Duck typing - Objects validated by behavior, not explicit interfaces
- React patterns - Hooks and components have different constraints
The question isn't whether SOLID applies to JavaScript, but how to translate these principles effectively.
Single Responsibility Principle (SRP)
Definition: A module should have one reason to change.
This translates most directly to JavaScript. Whether you're writing classes, functions, or React components, each should handle one concern.
React Component Example
Here's a common violation - a component handling multiple responsibilities:
This component has three reasons to change: data fetching logic, state management, or rendering requirements. Extract each concern:
Now each piece has one responsibility. The useUser hook can be tested independently and reused in other components. The UserCard renders user data without knowing where it came from. Changes to the API affect only useUser, not the presentation logic.
Common SRP Anti-pattern: God Objects
These functions have nothing in common. Split them into focused modules:
Open/Closed Principle (OCP)
Definition: Software entities should be open for extension, closed for modification.
In JavaScript, this means adding new functionality without changing existing code. The strategy pattern works well here.
Payment Processing Example
Here's the violation - adding new payment methods requires modifying existing code:
The functional approach using strategy pattern:
This works with TypeScript's structural typing. New processors just need to match the shape - no explicit interface declaration required in JavaScript, though TypeScript helps with compile-time safety.
OCP with Higher-Order Functions
Liskov Substitution Principle (LSP)
Definition: Objects should be replaceable with their subtypes without breaking the program.
LSP violations are common with inheritance. The classic example:
The solution is composition over inheritance:
LSP in React Components
Interface Segregation Principle (ISP)
Definition: Clients shouldn't depend on interfaces they don't use.
JavaScript's duck typing naturally supports this, but TypeScript interfaces and React props benefit from explicit segregation.
The Fat Interface Problem
Split into focused interfaces:
ISP in React Components
TypeScript Utility Types for ISP
Dependency Inversion Principle (DIP)
Definition: High-level modules shouldn't depend on low-level modules. Both should depend on abstractions.
This is critical for testability and flexibility.
The Problem: Direct Dependencies
Use constructor injection with abstractions:
DIP in React with Context API
When SOLID Becomes Overkill
Not all code benefits from strict SOLID adherence. Premature abstraction creates unnecessary complexity.
Overkill Example: Simple String Formatting
When to Apply SOLID Strictly
- Large codebases with multiple teams
- Libraries and frameworks with public APIs
- Long-lived enterprise applications
- Complex business logic requiring high testability
- Systems requiring flexibility in implementation swapping
When to Relax SOLID
- Prototypes and MVPs where speed matters more than architecture
- Small utility functions where abstraction overhead exceeds benefits
- Stable CRUD applications with unlikely requirement changes
- One-off scripts and tools with short lifespans
- When patterns haven't emerged - wait for duplication before abstracting
TypeScript's Role
TypeScript significantly enhances SOLID in JavaScript:
Key Takeaways
SOLID principles remain valuable in JavaScript but require adaptation:
- SRP translates directly to functions, modules, components, and hooks
- OCP works through composition and higher-order functions, not deep inheritance
- LSP matters more with TypeScript - dynamic typing allows duck typing
- ISP naturally supported by JavaScript's flexibility - TypeScript makes it explicit
- DIP essential for testability - constructor injection and Context API work well
React hooks align naturally with SOLID:
- Custom hooks enforce SRP
- Hooks compose without modification (OCP)
- Hook interfaces should be minimal (ISP)
- Dependencies injected via parameters or Context (DIP)
Balance pragmatism with principles:
- Small utilities don't need abstraction layers
- Wait for patterns to emerge before abstracting
- Prototypes can skip architecture for speed
- Large codebases benefit from strict SOLID adherence
TypeScript enhances SOLID:
- Interfaces make ISP and DIP explicit
- Type checking prevents LSP violations
- Generics enable type-safe abstraction
- Utility types help create focused interfaces
The goal isn't dogmatic adherence to SOLID principles, but understanding when they improve code quality versus when they introduce unnecessary complexity. In JavaScript's dynamic, functional environment, these principles provide valuable guidance when adapted thoughtfully to the language's strengths.
References
- typescriptlang.org - TypeScript Handbook and language reference.
- github.com - TypeScript project wiki (FAQ and design notes).
- developer.mozilla.org - MDN JavaScript reference and guides.
- react.dev - React official documentation.
- martinfowler.com - Martin Fowler on software architecture (index).
- developer.mozilla.org - MDN Web Docs (web platform reference).
- semver.org - Semantic Versioning specification.