Mastering SOLID Principles in TypeScript and JavaScript
← Back to Blog
codingtypescriptsolid-principles4 min read·March 29, 2026

Mastering SOLID Principles in TypeScript and JavaScript

Writing software is one thing. Writing software that’s easy to maintain, scalable, and doesn’t break every time you touch it; that’s a whole different story. That’s where the SOLID principles come in.

If you’ve ever wondered how to make your TypeScript or JavaScript projects cleaner and easier to work with, you’re in the right place. Let’s break these principles down in simple, human terms — no fluff, just clarity.

What Are SOLID Principles, Really?

SOLID is a handy acronym that represents five core ideas for writing better code. They are:

S — Single Responsibility Principle
O — Open/Closed Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle

Each principle helps you make your code more robust and flexible. Let’s take a deeper understanding of them.

1. Single Responsibility Principle (SRP)

In plain English: each class, module, or function should do one thing, just one.

When one piece of code tries to handle too many jobs, it becomes harder to test and even harder to change.

Example in TypeScript:

// Bad: Class with multiple responsibilities
class UserManager {
saveUser(user: { name: string; email: string }) {
// Save user to database
console.log(`Saving user ${user.name} to database`);
}

sendEmail(user: { name: string; email: string }, message: string) {
// Send email to user
console.log(`Sending email to ${user.email}: ${message}`);
}
}

// Good: Separate responsibilities
class UserRepository {
saveUser(user: { name: string; email: string }) {
console.log(`Saving user ${user.name} to database`);
}
}

class EmailService {
sendEmail(user: { name: string; email: string }, message: string) {
console.log(`Sending email to ${user.email}: ${message}`);
}
}
// Bad: Mixed responsibilities
class UserManager {
saveUser(user) {
console.log(`Saving user ${user.name} to database`);
}
sendEmail(user, message) {
console.log(`Sending email to ${user.email}: ${message}`);
}
}
// Good: Single responsibility
class UserRepository {
saveUser(user) {
console.log(`Saving user ${user.name} to database`);
}
}
class EmailService {
sendEmail(user, message) {
console.log(`Sending email to ${user.email}: ${message}`);
}
}

Here, one class handles fetching data, while the other deals with logging. If something breaks in logging, you don’t risk breaking user retrieval logic. Simple and safe.

2. Open/Closed Principle (OCP)

Your code should be open for extension but closed for modification.
That means you can add new behaviour without rewriting what already works.

Example in JavaScript:

// Bad: Modifying class to add new payment method
class PaymentProcessor {
processPayment(type: string, amount: number) {
if (type === "credit") {
console.log(`Processing credit payment of ${amount}`);
} else if (type === "paypal") {
console.log(`Processing PayPal payment of ${amount}`);
}
}
}

// Good: Open for extension, closed for modification
interface PaymentMethod {
process(amount: number): void;
}

class CreditPayment implements PaymentMethod {
process(amount: number) {
console.log(`Processing credit payment of ${amount}`);
}
}

class PayPalPayment implements PaymentMethod {
process(amount: number) {
console.log(`Processing PayPal payment of ${amount}`);
}
}

class PaymentProcessor {
constructor(private paymentMethod: PaymentMethod) {}

processPayment(amount: number) {
this.paymentMethod.process(amount);
}
}
// Bad: Modifying class
class PaymentProcessor {
processPayment(type, amount) {
if (type === "credit") {
console.log(`Processing credit payment of ${amount}`);
} else if (type === "paypal") {
console.log(`Processing PayPal payment of ${amount}`);
}
}
}
// Good: Using polymorphism
class CreditPayment {
process(amount) {
console.log(`Processing credit payment of ${amount}`);
}
}
class PayPalPayment {
process(amount) {
console.log(`Processing PayPal payment of ${amount}`);
}
}
class PaymentProcessor {
constructor(paymentMethod) {
this.paymentMethod = paymentMethod;
}
processPayment(amount) {
this.paymentMethod.process(amount);
}
}

Instead of touching the original Payment class, we just extend it. This makes your code easier to evolve as new features come in.

3. Liskov Substitution Principle (LSP)

This one sounds fancy, but it’s simple:
If you replace a parent class with one of its children, everything should still work fine.

Example in TypeScript:

class Bird {
fly() {
console.log('Flying');
}
}
class Sparrow extends Bird {}
function makeBirdFly(bird: Bird) {
bird.fly();
}
makeBirdFly(new Sparrow());

Sparrow can be used wherever Bird is expected, and nothing breaks. That’s LSP in action.

4. Interface Segregation Principle (ISP)

Don’t force classes to implement stuff they don’t need. Keep interfaces small and focused.

Example:

interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class Robot implements Workable {
work() {
console.log('Robot working');
}
}
class Human implements Workable, Eatable {
work() {
console.log('Human working');
}
eat() {
console.log('Human eating');
}
}

Here, Robot doesn’t eat—so it doesn’t need to implement that. Smaller, more specific interfaces make everyone’s life easier.

5. Dependency Inversion Principle (DIP)

This principle says that high-level code shouldn’t depend on low-level details.
Instead, both should depend on abstractions — like interfaces.

Example:

interface Database {
connect(): void;
}
class MySQLDatabase implements Database {
connect() {
console.log('Connected to MySQL');
}
}
class App {
constructor(private database: Database) {}
  start() {
this.database.connect();
}
}
const app = new App(new MySQLDatabase());
app.start();

Now your app doesn’t care which database it uses. You can easily swap MySQL for MongoDB or Postgres without changing the main logic.

Why SOLID Matters for You

Following SOLID principles isn’t just about writing “pretty” code; it’s about writing code that lasts.
When your app grows, these principles help keep things manageable. You spend less time debugging and more time building cool new features.

With TypeScript, it gets even better. Its type system naturally supports these ideas, making your architecture stronger and less error-prone.

Wrapping Up

Learning SOLID principles takes a bit of practice, but the payoff is huge. Your projects become easier to scale, test, and maintain.

So next time you start a new JavaScript or TypeScript project, try applying one SOLID principle at a time. Over time, it’ll become second nature — and your future self (and your teammates) will thank you for it.

Keep coding smart, not just hard.

Originally published on Medium

Open to work

Got a project in mind?

Open to freelance work, full-time roles, and interesting collaborations.