SOLID Principles in Practice
Apply SOLID principles to real TypeScript code: dependency injection, interface segregation, Liskov substitution in practice, and knowing when they add unnecessary complexity.
SOLID principles - Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion - guide object-oriented design. Applied pragmatically (not dogmatically), they reduce coupling, improve testability, and enable extension without modification.
SOLID principles are tools, not rules. Apply them when they solve a real problem - overly SOLID code can be over-engineered. Prioritize: SRP and DI (most universally useful), then ISP and OCP. Apply LSP naturally by designing good abstractions.
Workflow
- Single Responsibility PrincipleA class with one reason to change is easier to understand, test, and modify without side effects.
- Open/Closed PrincipleCode open for extension but closed for modification prevents regression when adding features.
- Liskov Substitution PrincipleIf you need to check what subtype you have before calling a method, you've violated LSP.
- Interface Segregation PrincipleFat interfaces force implementations to depend on methods they don't use.
- Dependency Inversion PrincipleHigh-level modules should not depend on low-level implementations - depend on abstractions to enable testing and swapping.
Output Format
SRP: one class/function per concept, OCP: strategy/plugin extension points, LSP: no instanceof checks in polymorphic code, ISP: small focused interfaces, DIP: constructor injection with interface types.
Examples
DIP in TypeScript: `interface NotificationService { send(userId: string, message: string): Promise<void>; } class EmailNotificationService implements NotificationService { async send(userId, message) { await sgMail.send({...}) } } class UserService { constructor(private notifications: NotificationService) {} async deactivate(userId: string) { await this.notifications.send(userId, "Account deactivated") } }`ISP: `interface Readable { read(): Promise<string> } interface Writable { write(data: string): Promise<void> } interface ReadWritable extends Readable, Writable {} class FileStorage implements ReadWritable {} class ReadOnlyCache implements Readable {}`OCP with plugin: `const processors: Map<string, Handler> = new Map(); function registerHandler(event: string, handler: Handler) { processors.set(event, handler) } // adding new event types requires only registerHandler, not modifying dispatch logic`Edge Cases & Gotchas
- YAGNI vs OCP: don't apply OCP to things that haven't needed to change yet. Premature abstraction is as harmful as premature optimization. Apply OCP after the second time you need to extend the same thing
- Functional code: SOLID was defined for OOP. In functional programming, SRP applies to functions, DIP maps to function injection, and OCP maps to higher-order functions. The principles translate but look different
- DI frameworks: for small to medium apps, manual constructor injection is sufficient and clearer than a DI container. Only introduce InversifyJS/tsyringe for large apps with complex dependency graphs
When to Break These Rules
- For scripts, one-off utilities, and small functions, SOLID adds ceremony without benefit. Also: data-processing pipelines are often clearest as a linear sequence of transforms, not as OOP hierarchies.
Reference Files
SOLID principles: https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-designClean Architecture (book): Robert C. Martin, 2017DI without frameworks: https://www.jamesshore.com/v2/blog/2006/dependency-injection-demystified