SOLID TypeScript Design
Apply these rules when you design, refactor, or review TypeScript types,
classes, interfaces, services, and modules.
Worked before/after snippets: examples.md. Lookup tables and
idioms: reference.md.
Keywords in this document:
| Keyword |
Meaning |
| must |
Required. Do not deviate. |
| must not |
Forbidden. |
| should |
Strongly preferred unless a reviewer agrees otherwise. |
| may |
Optional. |
1. Scope
- Principles target types, classes, interfaces, and modules—not only OOP
class trees.
- Prefer composition and interfaces over deep inheritance.
- Do not force class hierarchies onto React UI components when a simpler
module or hook split is enough.
- Treat SOLID as guidelines: apply with judgment when a strict split hurts
clarity or performance for a tiny, stable unit.
- Use with tests, refactoring, and clear boundaries—not as the only design tool.
2. S — Single Responsibility Principle (SRP)
Definition: A module or class must have one reason to change.
Rules:
- A type must own one cohesive concern (for example user persistence, email
delivery, or activity logging—not all three).
- When a change request would touch unrelated behaviors in the same type, you
must split those behaviors into separate types or modules.
- A type should expose a focused public API that matches that concern.
- A type must not mix domain logic with orthogonal side effects (I/O,
notifications, formatting) when those side effects change for different
reasons.
Smell → fix: God class / mixed create+email+log → extract focused
collaborators and compose them at the call site or via injection.
3. O — Open/Closed Principle (OCP)
Definition: Software entities should be open for extension and closed
for modification.
Rules:
- New variants of behavior should be added as new implementations of an
interface (or strategy), not by editing a growing
if / switch on type
strings.
- Shared orchestrators (processors, calculators) must depend on an
abstraction and call it; they must not encode every concrete case.
- Existing stable modules should stay unchanged when a new case appears,
aside from wiring the new implementation.
- You may modify a module when fixing a bug or changing shared contract
behavior; OCP does not forbid all edits.
Smell → fix: if (type === "credit") … else if (type === "paypal") →
PaymentMethod interface + per-method classes + one processor.
4. L — Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without
breaking callers that rely on the base contract.
Rules:
- A subtype must honor the base type’s expectations (inputs accepted,
outputs promised, errors thrown).
- A subtype must not throw “not supported” for inherited operations, weaken
preconditions, or strengthen postconditions in ways callers cannot rely on.
- When a subtype cannot honestly fulfill the base contract, you must
redesign: narrower base, separate interfaces, or no inheritance.
- Prefer capability-based types (for example
move() with flying vs walking
subtypes) over a base that promises behavior only some subtypes can provide.
Smell → fix: Penguin extends Bird with fly() throwing → split flying /
non-flying capabilities, or use a shared move() contract both can meet.
5. I — Interface Segregation Principle (ISP)
Definition: Clients must not be forced to depend on methods they do not
use.
Rules:
- Prefer small role interfaces over one fat interface that every implementer
must satisfy.
- An implementer must only implement roles it actually supports.
- Callers should depend on the narrowest interface they need.
- Empty, no-op, or throw-stub implementations of unused methods must not be
used to “satisfy” a fat interface.
Smell → fix: Worker { work(); eat(); } forced on Robot → Workable and
Eatable; robot implements only Workable.
6. D — Dependency Inversion Principle (DIP)
Definition: High-level modules must not depend on low-level modules.
Both must depend on abstractions.
Rules:
- Application / domain services must depend on interfaces (or abstract
types), not concrete infrastructure classes.
- Concrete adapters (databases, HTTP clients, mailers) must implement those
abstractions.
- Dependencies should be provided via constructor injection (or an
equivalent explicit injection point), not constructed with
new inside the
high-level type.
- Swapping an implementation should not require editing the high-level
consumer—only wiring.
Smell → fix: UserService does this.db = new MySQLDatabase() → inject
Database; provide MySQL, Postgres, or in-memory implementations.
7. Design and review workflow
When designing or reviewing TypeScript structure:
- Name the change reasons for each type. More than one unrelated reason →
apply SRP.
- List upcoming variants. If new cases will keep editing a switch → apply
OCP with an interface + implementations.
- Check substitution. For every
extends / implements used polymorphically,
ask whether every subtype is safe where the base is expected → apply LSP.
- Trim interfaces. If a client mocks or stubs methods it never calls →
apply ISP.
- Invert dependencies. If a service constructs or imports a concrete
infrastructure type → apply DIP with an interface and injection.
- Verify with tests. Prefer unit tests against abstractions; swap fakes at
the injection boundary.
8. Pre-merge checklist
SRP
OCP
LSP
ISP
DIP
9. Cross-references
- Before/after TypeScript for each principle: examples.md.
- Smells, misconceptions, and TypeScript idioms: reference.md.
1---2name: typescript-solid-design3description: Apply SOLID design principles when writing, refactoring, or reviewing TypeScript modules, classes, interfaces, and services. Use when designing maintainable architecture, splitting responsibilities, extending behavior without modification, fixing inheritance contracts, segregating interfaces, injecting dependencies, or addressing code smells in TypeScript.4---56# SOLID TypeScript Design78Apply these rules when you design, refactor, or review TypeScript types,9classes, interfaces, services, and modules.1011Worked before/after snippets: [examples.md](examples.md). Lookup tables and12idioms: [reference.md](reference.md).1314Keywords in this document:1516| Keyword | Meaning |17| --- | --- |18| **must** | Required. Do not deviate. |19| **must not** | Forbidden. |20| **should** | Strongly preferred unless a reviewer agrees otherwise. |21| **may** | Optional. |2223---2425## 1. Scope2627- Principles target **types, classes, interfaces, and modules**—not only OOP28 class trees.29- Prefer **composition and interfaces** over deep inheritance.30- Do **not** force class hierarchies onto React UI components when a simpler31 module or hook split is enough.32- Treat SOLID as guidelines: apply with judgment when a strict split hurts33 clarity or performance for a tiny, stable unit.34- Use with tests, refactoring, and clear boundaries—not as the only design tool.3536---3738## 2. S — Single Responsibility Principle (SRP)3940**Definition:** A module or class **must** have one reason to change.4142**Rules:**4344- A type **must** own one cohesive concern (for example user persistence, email45 delivery, or activity logging—not all three).46- When a change request would touch unrelated behaviors in the same type, you47 **must** split those behaviors into separate types or modules.48- A type **should** expose a focused public API that matches that concern.49- A type **must not** mix domain logic with orthogonal side effects (I/O,50 notifications, formatting) when those side effects change for different51 reasons.5253**Smell → fix:** God class / mixed create+email+log → extract focused54collaborators and compose them at the call site or via injection.5556---5758## 3. O — Open/Closed Principle (OCP)5960**Definition:** Software entities **should** be open for extension and closed61for modification.6263**Rules:**6465- New variants of behavior **should** be added as new implementations of an66 interface (or strategy), not by editing a growing `if` / `switch` on type67 strings.68- Shared orchestrators (processors, calculators) **must** depend on an69 abstraction and call it; they **must not** encode every concrete case.70- Existing stable modules **should** stay unchanged when a new case appears,71 aside from wiring the new implementation.72- You **may** modify a module when fixing a bug or changing shared contract73 behavior; OCP does not forbid all edits.7475**Smell → fix:** `if (type === "credit") … else if (type === "paypal")` →76`PaymentMethod` interface + per-method classes + one processor.7778---7980## 4. L — Liskov Substitution Principle (LSP)8182**Definition:** Subtypes **must** be substitutable for their base types without83breaking callers that rely on the base contract.8485**Rules:**8687- A subtype **must** honor the base type’s expectations (inputs accepted,88 outputs promised, errors thrown).89- A subtype **must not** throw “not supported” for inherited operations, weaken90 preconditions, or strengthen postconditions in ways callers cannot rely on.91- When a subtype cannot honestly fulfill the base contract, you **must**92 redesign: narrower base, separate interfaces, or no inheritance.93- Prefer capability-based types (for example `move()` with flying vs walking94 subtypes) over a base that promises behavior only some subtypes can provide.9596**Smell → fix:** `Penguin extends Bird` with `fly()` throwing → split flying /97 non-flying capabilities, or use a shared `move()` contract both can meet.9899---100101## 5. I — Interface Segregation Principle (ISP)102103**Definition:** Clients **must not** be forced to depend on methods they do not104use.105106**Rules:**107108- Prefer **small role interfaces** over one fat interface that every implementer109 must satisfy.110- An implementer **must** only implement roles it actually supports.111- Callers **should** depend on the narrowest interface they need.112- Empty, no-op, or throw-stub implementations of unused methods **must not** be113 used to “satisfy” a fat interface.114115**Smell → fix:** `Worker { work(); eat(); }` forced on `Robot` → `Workable` and116`Eatable`; robot implements only `Workable`.117118---119120## 6. D — Dependency Inversion Principle (DIP)121122**Definition:** High-level modules **must not** depend on low-level modules.123Both **must** depend on abstractions.124125**Rules:**126127- Application / domain services **must** depend on interfaces (or abstract128 types), not concrete infrastructure classes.129- Concrete adapters (databases, HTTP clients, mailers) **must** implement those130 abstractions.131- Dependencies **should** be provided via **constructor injection** (or an132 equivalent explicit injection point), not constructed with `new` inside the133 high-level type.134- Swapping an implementation **should** not require editing the high-level135 consumer—only wiring.136137**Smell → fix:** `UserService` does `this.db = new MySQLDatabase()` → inject138`Database`; provide MySQL, Postgres, or in-memory implementations.139140---141142## 7. Design and review workflow143144When designing or reviewing TypeScript structure:1451461. **Name the change reasons** for each type. More than one unrelated reason →147 apply SRP.1482. **List upcoming variants.** If new cases will keep editing a switch → apply149 OCP with an interface + implementations.1503. **Check substitution.** For every `extends` / `implements` used polymorphically,151 ask whether every subtype is safe where the base is expected → apply LSP.1524. **Trim interfaces.** If a client mocks or stubs methods it never calls →153 apply ISP.1545. **Invert dependencies.** If a service constructs or imports a concrete155 infrastructure type → apply DIP with an interface and injection.1566. **Verify with tests.** Prefer unit tests against abstractions; swap fakes at157 the injection boundary.158159---160161## 8. Pre-merge checklist162163**SRP**164165- [ ] Each new/changed type has one clear reason to change?166- [ ] Side effects (mail, logging, persistence) are not jammed into unrelated167 domain types?168169**OCP**170171- [ ] New variants added without rewriting a central type-switch?172- [ ] Extension points use interfaces or strategies?173174**LSP**175176- [ ] No subtype throws or no-ops inherited behavior callers expect?177- [ ] Inheritance only where substitution is honest?178179**ISP**180181- [ ] Interfaces are role-sized; no fat interface forcing unused methods?182- [ ] Callers depend on the narrowest contract they need?183184**DIP**185186- [ ] High-level code depends on abstractions?187- [ ] Concretes injected, not constructed inside high-level types?188189---190191## 9. Cross-references192193- Before/after TypeScript for each principle: [examples.md](examples.md).194- Smells, misconceptions, and TypeScript idioms: [reference.md](reference.md).