DEV Community

Cover image for The OOP Fight : JavaScript Vs TypeScript
Rawan Aboegila
Rawan Aboegila

Posted on • Updated on

The OOP Fight : JavaScript Vs TypeScript

When I first started learning about JavaScript, I couldn’t help but notice how different it interacts with objects and entities compared to other languages that support object-oriented programming like C++ and Java, and how JavaScript is always categorized as an OOP Language by many. However, it has a different nature, as it is a special type of OOP that is prototype-based not the classical class-based OOP that we are mostly familiar with. That means that JS supports both functional and object-oriented programming; and that's why it handles objects and entities in a slightly different way.

However, despite the fact that TypeScript is a superset of JavaScript, it offers several advantages over its predecessor, particularly when it comes to object-oriented programming. In this article, we will explore how TypeScript has enhanced OOP capabilities in comparison to JavaScript, and how it conforms to the SOLID principles with code examples.

Table Of Contents


TypeScript Key Features

First : Strongly Typed Language

One of the most significant advantages of TypeScript over JavaScript is that it is a strongly typed language. This means that TS enforces types on variables, functions, and classes, ensuring that the code is more robust and less error-prone. In contrast, JavaScript is a dynamically typed language, which means that variables can change their types at runtime, leading to more bugs and less maintainable code.

// JavaScript
function add(a, b) {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode
// TypeScript
function add(a: number, b: number): number {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the TypeScript function add has types for its parameters (a and b) and return value. This makes it easier to understand the function's behavior and catch errors at compile-time.


Second : Classes and Interfaces

Classes allow you to define blueprints for objects with properties and methods, while interfaces define the structure and types of objects. These features are mostly not available in JavaScript, which uses prototype-based inheritance instead of class-based inheritance.


Classes

According to MDN, the JS class implementation is introduced in 2015, in ES6. It is based on the notation that classes are just special functions

Classes are in fact "special functions", and just as you can define function expressions and function declarations, a class can be defined in two ways: a class expression or a class declaration.

Constructor Function in JavaScript
// JavaScript
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise.`);
}
Enter fullscreen mode Exit fullscreen mode

Since TypeScript is a subset of JavaScript, under the hood, it implements classes the same way JS does. However, due to TS strongly typed nature, there are several properties added on top of JS that enhances readability, maintainability and reduces errors. In other words, TypeScript has full support for that syntax and also adds features on top of it, like member visibility, abstract classes, generic classes, arrow function methods, and a few others.


Classes in JavaScript
// JavaScript

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}
Enter fullscreen mode Exit fullscreen mode

Classes in TypeScript
class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  speak(): void {
    console.log(`${this.name} makes a noise.`);
  }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, there isn't a big difference in the general syntax between JS and TS when it comes to classes. However TypeScript Classes can be considered more readable, maintainable and less error prune. As we go through the key features of TypeScript, we will explore how TypeScript has full support for the class-based OOP.


Interfaces

JavaScript does not have support for interfaces, as it's inheritance is based on objects and not classes and due to it's dynamic nature. Moreover, JavaScript uses duck typing.

If it walks like a duck, and quacks like a duck, as far as JS cares, it’s a duck.

On the other hand, TypeScript has support for interfaces. interface and type are how TS can enforce structure on objects, making sure that objects have the expected properties.

TypeScript interfaces supports interface extending and adding fields to an existing interface

// JavaScript
function printLabel(labelObj) {
  console.log(labelObj.label);
}
Enter fullscreen mode Exit fullscreen mode
// TypeScript
interface LabelObj {
  label: string;
}

function printLabel(labelObj: LabelObj): void {
  console.log(labelObj.label);
}

printLabel({ label:"name" })
Enter fullscreen mode Exit fullscreen mode

In the above example, the JavaScript printLabel function takes an object with a label property. In TypeScript, an interface is defined for this object to ensure that it has a label property of type string. This makes it easier to catch errors at compile-time and write more maintainable code.

Type Aliases

Types are another example of how Typescript enforces object structure.

According to TypeScript Documentation :

Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable

Here's an example

interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

interface Animal {
favFood: string

}
Enter fullscreen mode Exit fullscreen mode
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

interface Animal {
favFood: string

}
 // Error: Duplicate identifier 'Animal'.

Enter fullscreen mode Exit fullscreen mode



Third : Access Modifiers

TypeScript also offers access modifiers, which allow you to restrict access to certain properties and methods of a class. Access modifiers like private and protected help to enforce encapsulation and prevent unauthorized access to the internal state of an object. JavaScript does not have access modifiers, making it more difficult to achieve encapsulation and maintain a clean code base.


Here is an example of how TypeScript access modifiers can help with encapsulation:

// JavaScript

function Person(age) {
   This.age =age;
}


Person.prototype.getAge = function() {
   Return this.age
}
Enter fullscreen mode Exit fullscreen mode



Let's say that you want Person to be a class with a a private property called age. You want to create a method called getAge() that returns the value of the age property, but you don't want to allow direct access to the age property from outside the class.

// TypeScript
class Person {
 private age: number;


 constructor(age: number) {
   this.age = age;
 }


 public get getAge(): number {
   return this.age;
 }
}

Enter fullscreen mode Exit fullscreen mode

By using access modifiers like private, public, and protected, you can control access to the internal state of an object, ensuring that it is only modified in a controlled manner. This helps to maintain encapsulation, which is an essential principle of OOP that promotes modularity, maintainability, and robustness.

Moreover, TypeScript has two additional access modifiers than the popular three mentioned above: static and readonly

  • static properties: they can only be accessed from the class, not from an object instantiated from it and they can not be inherited.
  • readonly properties: these properties can not be modified after initialization, they can only be read. They must be set at the class declaration or inside the constructor

The example shows how the TS uses access modifiers inside the class. Note that this keyword can not be used to reference static properties and methods.

//TypeScript

class Employee {
private name: string;
readonly ssn : string;
private static  count: number =  0;


constructor(name:string, ssn:string){
this.name = name
this.ssn = ssn
Employee.count =+1;
}
public getName():string {
return this.name;
}
public static getCount():number {
   return Employee.count;
}
public getSSN():string{
   return this.ssn;
}


};


let emp:Employee = new Employee("Mike", "836527496");


//emp.count; // 'count' does not exist on type 'Employee'
//emp.ssn=5; // Cannot assign to 'ssn' because it is a read-only property

Enter fullscreen mode Exit fullscreen mode



Now as mentioned before, JS class support was introduced in ES6, and with it some features of classes:

  • static keyword

  • private access modifier using # notation

  • get and set keywords

Here's an example demonstrating the added features:

//JavaScript

class Time {
    #hour = 0
    #minute = 0
    #second = 0
    static #GMT = 0
    constructor(hour, minute, second) {
        this.#hour = hour
        this.#minute = minute
        this.#second = second
    }
    static  get displayGMT() {
        return Time.#GMT
    }

    static set setGMT(gmt){
      Time.#GMT=gmt
    }
    get getHour(){
      return this.#hour
    }
   set setHour(hour){
       this.#hour=hour
    }

}
//console.log(Time.getHour()) // Error: Time.getHour is not a function
console.log(Time.getHour) //undefined
console.log(Time.displayGMT) //0
Time.setGMT = 2
myTime = new Time()
console.log(Time.#hour) //Error: reference to undeclared private field or method #hour
console.log(myTime.#hour) //Error: reference to undeclared private field or method #hour
console.log(Time.displayGMT) //2
Enter fullscreen mode Exit fullscreen mode

Despite having these features, their behavior is quite confusing to grasp at the first glance, moreover it's readability is less than it's TypeScript equivalent.

//TypeScript

class Time {
    private hour: number = 0
    private minute: number = 0
    private second: number = 0
    private static GMT: number = 0
    constructor(hour: number, minute: number, second: number) {
        this.hour = hour
        this.minute = minute
        this.second = second
    }
    static get displayGMT(): number {
        return Time.GMT
    }

    static set setGMT(gmt: number) {
        Time.GMT = gmt
    }
    get getHour(): number {
        return this.hour
    }
    set setHour(hour: number) {
        this.hour = hour
    }

}

//console.log(Time.getHour()) //Property 'getHour' does not exist on type 'typeof Time'.
console.log(Time.displayGMT) //0
Time.setGMT = 2
//myTime = new Time() //Expected 3 arguments, but got 0.
const myTime: Time = new Time(1, 2, 3)
//console.log(Time.hour) //Property 'hour' does not exist on type 'typeof Time'
//console.log(myTime.hour) //Property 'hour' is private and only accessible within class 'Time'.
console.log(Time.displayGMT) //2
Enter fullscreen mode Exit fullscreen mode



Fourth : Inheritance and Polymorphism

TypeScript supports inheritance and polymorphism, two essential concepts in OOP. Inheritance allows you to create a hierarchy of classes, with each subclass inheriting properties and methods from its parent class.

Polymorphism allows you to use a subclass object in place of a parent class object, making the code more flexible and reusable. JavaScript does support inheritance and polymorphism, but it uses a less intuitive and more verbose syntax.

// JavaScript
function Animal(name) {
 this.name = name;
}


Animal.prototype.speak = function() {
 console.log(`${this.name} makes a noise.`);
}


function Dog(name) {
 Animal.call(this, name);
}


Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;


Dog.prototype.speak = function() {
 console.log(`${this.name} barks.`);
}

Enter fullscreen mode Exit fullscreen mode

In the JavaScript example, inheritance is implemented using prototype-based inheritance, which can be difficult to read and maintain.

// TypeScript
class Animal {
 name: string;


 constructor(name: string) {
   this.name = name;
 }


 speak(): void {
   console.log(`${this.name} makes a noise.`);
 }
}


class Dog extends Animal {
 constructor(name: string) {
   super(name);
 }


 speak(): void {
   console.log(`${this.name} barks.`);
 }
}

Enter fullscreen mode Exit fullscreen mode

In TypeScript, inheritance is implemented using the extends keyword, which makes the code more readable and easier to maintain.



With the introduction of classes in ES6, extends keyword became supported as well with class implementations.

Here's an example:

class Cat {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(`${this.name} roars.`);
  }
}

const myLion = new Lion("Fuzzy");
// Fuzzy makes a noise.

myLion.speak();  // Fuzzy roars.


Enter fullscreen mode Exit fullscreen mode



Fifth : Abstract Classes

TypeScript supports abstract classes, which are classes that cannot be instantiated directly. Abstract classes can only be used as base classes for other classes. This allows developers to define common functionality for a group of related classes without having to implement the functionality in each class.

Here's an example:

// JavaScript
 function Vehicle()  
{  
    this.vehicleName="vehicleName";  
    throw new Error("You cannot create an instance of Abstract Class");  
}  
Vehicle.prototype.display=function()  
{  
    return "Vehicle is: "+this.vehicleName;  
}  
//Creating a constructor function  
function Bike(vehicleName)  
{  
    this.vehicleName=vehicleName;  
}  
//Creating object without using the function constructor  
Bike.prototype=Object.create(Vehicle.prototype); 

Enter fullscreen mode Exit fullscreen mode

There is no direct JS Abstract class implementation, however there are ways/ workarounds that can be implemented to apply abstraction. Here in the JavaScript example above, we created a constructor function that throws an error if the constructor is called. And when it comes to the subclass Bike, using it’s prototype and accessing the Object create function to set it to the base class prototype.

// TypeScript
abstract class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  abstract speak(): void;
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  speak(): void {
    console.log(`${this.name} barks.`);
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }

  speak(): void {
    console.log(`${this.name} meows.`);
  }
}

Enter fullscreen mode Exit fullscreen mode

Unlike JavaScript, Typescript offers easy, direct and readable class abstraction implementation.




Sixth : Generics

TypeScript also supports generics, which allow you to create reusable code that can work with different types of data. Generics are especially useful when working with collections like arrays, where you may want to define a function or class that can work with any type of data. JavaScript does not have generics, making it more difficult to write generic code.

Here's a simple example of generics in TS:

// TypeScript
function identity<T>(arg: T): T {
 return arg;
}


let output1 = identity<string>("Hello");
let output2 = identity<number>(42);

Enter fullscreen mode Exit fullscreen mode



However, this example can easily be created in JS with similar results, thus doesn’t really show why Generics are important, why their absence from JavaScript makes it difficult to write reusable strongly typed code.

Here's how :

// JavaScript
function identity(arg) {
   return arg;
}
let output1 = identity("Hello");
let output2 = identity(42);
Enter fullscreen mode Exit fullscreen mode



Now let's see their power in the example below:

//JavaScript

function getArray(arr) {
   return new Array().concat(arr);
}
let myNumArr = getArray([10, 20, 30]);
let myStrArr = getArray(["Hello", "JavaScript"]);
myNumArr.push(40); // Correct 
myNumArr.push("Hello OOP"); // Correct 
myStrArr.push("Hello TypeScript"); // Correct 
myStrArr.push(40); // Correct 
console.log(myNumArr); // [10, 20, 30, 40, "Hello OOP"] 
console.log(myStrArr); // ["Hello", "JavaScript", "Hello TypeScript", 40]

Enter fullscreen mode Exit fullscreen mode



In the above example, the getArray() function accepts an array. The getArray() function creates a new array, concatenates items to it and returns this new array. Since JavaScript doesn’t define data type enforcement, we can pass any type of items to the function. But, this may not be the correct way to add items. We have to add numbers to myNumArr and the strings to myStrArr, but we do not want to add numbers to the myStrArr or vice-versa.

To solve this, TypeScript introduced generics. With generics, the type variable only accepts the particular type that the user provides at declaration.

Here's the TypeScript to the previous example:

//TypeScript

function getArray<T>(arr: T[]): T[] {
    return new Array<T>().concat(arr);
}
let myNumArr = getArray<number>([10, 20, 30]);
let myStrArr = getArray<string>(["Hello", "JavaScript"]);
myNumArr.push(40); // Correct  
myNumArr.push("Hi! OOP"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'  
myStrArr.push("Hello TypeScript"); // Correct  
myStrArr.push(50); // Error: Argument of type 'number' is not assignable to parameter of type 'string'

console.log(myNumArr);
console.log(myStrArr);
Enter fullscreen mode Exit fullscreen mode



Seventh : Intersection and Union Types

TypeScript supports intersection and union types, which are a way to combine types to create new types. Intersection types allow developers to create types that have all the properties and methods of two or more types. Union types allow developers to create types that can be one of two or more types.

Here's an example:

// TypeScript
interface Dog {
  name: string;
  breed: string;
}

interface Cat {
  name: string;
  color: string;
}

type Pet = Dog & Cat; // Intersection
type DogOrCat = Dog | Cat; // Union

let myPet: Pet = { name: "Fluffy", breed: "Poodle", color: "White" }; 
// Error: Property 'color' is missing in type 'Pet' but required in type 'Cat'.

let myDogOrCat: DogOrCat = { name: "Mittens", breed: "Tabby" };

Enter fullscreen mode Exit fullscreen mode

In the above example, the Dog and Cat interfaces are defined with different properties. The Pet type is defined as an intersection of the Dog and Cat interfaces, which means it has all the properties of both interfaces. The DogOrCat type is defined as a union of the Dog and Cat interfaces, which means it can be either a dog or a cat. The myPet variable is assigned an object that has all the properties of both the Dog and Cat interfaces, but TypeScript throws a compiler error because the color property is missing. The myDogOrCat variable is assigned an object that has the properties of the Dog interface.

Note : It's not possible to define intersection and union types in JavaScript




Eighth : Decorators

TypeScript supports decorators, which are a way to add metadata to classes, methods, and properties. Decorators allow developers to write code that is more declarative and flexible.

Note: Several libraries and frameworks are built based on this powerful feature, for example: Angular and Nestjs.

Here is a simple method decorator example to demonstrate the syntax:

function myDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 console.log(`Decorating ${target.constructor.name}.${propertyKey}`);
}
class MyClass {
 @myDecorator
 myMethod() {
   console.log("Hello, world!");
 }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the myDecorator function is defined with three parameters:

  • target (the class constructor)
  • propertyKey (the name of the decorated method)
  • descriptor (an object that describes the decorated method).

The myMethod method of the MyClass class is decorated with the myDecorator function. When the myMethod method is called, the decorator logs a message to the console.

Now let’s review a more advanced example that shows an actual decorator use case, this example uses parameter decorator and method decorator

import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
 let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
 existingRequiredParameters.push(parameterIndex);
 Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
 let method = descriptor.value!;
 descriptor.value = function () {
   let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
   console.log("paramas", requiredParameters)
   if (requiredParameters) {
     for (let parameterIndex of requiredParameters) {
       if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
         throw new Error("Missing required argument.");
       }
     }
   }
   return method.apply(this, arguments);
 };
}


class BugReport {
 type = "report";
 title: "string;"
 constructor(t: string) {
   this.title = t;
 }


 @validate
 print(@required verbose: boolean) {
   if (verbose) {
     return `type: ${this.type}\ntitle: "${this.title}`;"
   } else {
    return this.title;
   }
 }
}


const br = new BugReport("Bug Report Message")


console.log(br.print()) // [ERR]: Missing required argument.
console.log(br.print(true)) // [LOG]: "type: report title: "Bug Report Message\""
Enter fullscreen mode Exit fullscreen mode

The @required decorator adds a metadata entry that marks the parameter as required. The @validate decorator then wraps the existing greet method in a function that validates the arguments before invoking the original method.

It’s important to mention that decorators, while useful and revolutionary, are an experimental feature in TypeScript. To use them you need to set the compiler target in tsconfig.ts to ES5 or higher and enable the experimentalDecorators, moreover, in the previous example you will also need to install the reflect-metadata library and enable the emitDecoratorMetadata in the tsconfig.ts file as well

Note
I highly suggest that you check out the full documentation and article on decorators for more detailed explanations and examples. To do so, follow these links:




SOLID Principles

SOLID is an acronym for five Object-Oriented design principles that aim to make software systems more maintainable, extensible, and robust. These principles are:

  • S - Single Responsibility Principle (SRP)
  • O - Open-Closed Principle (OCP)
  • L - Liskov Substitution Principle (LSP)
  • I - Interface Segregation Principle (ISP)
  • D - Dependency Inversion Principle (DIP)

Let's see how TypeScript conforms to these principles with code examples.

First : Single Responsibility Principle (SRP)

The SRP states that a class should have only one reason to change. In other words, a class should be responsible for only one thing. This principle helps to promote modularity, maintainability, and testability.
In TypeScript, you can use classes and modules to achieve the SRP.

Here's an example:

// Account.ts
export class Account {
  private balance: number;

  constructor(balance: number) {
    this.balance = balance;
  }

  public getBalance(): number {
    return this.balance;
  }

  public deposit(amount: number): void {
    this.balance += amount;
  }

  public withdraw(amount: number): void {
    if (amount > this.balance) {
      throw new Error('Insufficient funds');
    }

    this.balance -= amount;
  }
}

// Bank.ts
import { Account } from './Account';

export class Bank {
  private accounts: Account[];

  constructor() {
    this.accounts = [];
  }

  public openAccount(balance: number): Account {
    const account = new Account(balance);
    this.accounts.push(account);
    return account;
  }

  public closeAccount(account: Account): void {
    const index = this.accounts.indexOf(account);
    if (index === -1) {
      throw new Error('Account not found');
    }

    this.accounts.splice(index, 1);
  }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Account class is responsible for managing an account's balance, while the Bank class is responsible for managing a collection of accounts. By separating these responsibilities into different classes, we ensure that each class has only one reason to change, making the code more modular, maintainable, and testable.




Second : Open-Closed Principle (OCP)

The OCP states that a class should be open for extension but closed for modification. In other words, you should be able to add new functionality to a class without modifying its existing code. This principle helps to promote extensibility and maintainability.
In TypeScript, you can use inheritance and interfaces to achieve the OCP.

Here's an example:

// Shape.ts
export interface Shape {
  getArea(): number;
}

// Rectangle.ts
import { Shape } from './Shape';

export class Rectangle implements Shape {
  private width: number;
  private height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  public getArea(): number {
    return this.width * this.height;
  }
}

// Circle.ts
import { Shape } from './Shape';

export class Circle implements Shape {
  private radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  public getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Shape interface defines a contract for shapes that have an area. The Rectangle and Circle classes implement the Shape interface, providing their own implementations of the getArea() method. By using the Shape interface, we can add new shapes to the system without modifying the existing code, making the code more extensible and maintainable.




Third : Liskov Substitution Principle (LSP)

The LSP states that a subclass should be substitutable for its parent class without affecting the correctness of the program. In other words, you should be able to use a subclass object in place of a parent class object without causing any problems. This principle helps to promote polymorphism and extensibility.
In TypeScript, you can use inheritance and interfaces to achieve the LSP

Here's an example:

// Vehicle.ts
export class Vehicle {
  public startEngine(): void {
    console.log('Engine started');
  }

  public stopEngine(): void {
    console.log('Engine stopped');
  }
}

// Car.ts
import { Vehicle } from './Vehicle';

export class Car extends Vehicle {
  public drive(): void {
    console.log('Driving');
  }
}

// RaceCar.ts
import { Car } from './Car';

export class RaceCar extends Car {
  public drive(): void {
    console.log('Driving fast

Enter fullscreen mode Exit fullscreen mode

In this example, the Vehicle class defines a basic set of methods for a vehicle, including starting and stopping the engine. The Car class extends the Vehicle class, adding a drive() method that is specific to cars. The RaceCar class extends the Car class, adding a drive() method that is specific to race cars.

By using inheritance and the LSP, we can use a RaceCar object in place of a Vehicle or Car object without affecting the correctness of the program. This promotes polymorphism and extensibility, making the code more flexible and reusable.




Fourth : Interface Segregation Principle (ISP)

The ISP states that a client should not be forced to depend on methods it does not use. In other words, you should design interfaces that are specific to each client's needs, rather than creating a monolithic interface that includes methods for all clients. This principle helps to promote modularity and maintainability.
In TypeScript, you can use interfaces to achieve the ISP.

Here's an example:

// Database.ts
export interface Database {
  connect(): void;
  disconnect(): void;
  query(sql: string): any[];
}

// SqliteDatabase.ts
import { Database } from './Database';

export class SqliteDatabase implements Database {
  public connect(): void {
    console.log('Connecting to SQLite database');
  }

  public disconnect(): void {
    console.log('Disconnecting from SQLite database');
  }

  public query(sql: string): any[] {
    console.log(`Executing SQL query: ${sql}`);
    return [];
  }
}

// RedisDatabase.ts
import { Database } from './Database';

export class RedisDatabase implements Database {
  public connect(): void {
    console.log('Connecting to Redis database');
  }

  public disconnect(): void {
    console.log('Disconnecting from Redis database');
  }

  public query(sql: string): any[] {
    throw new Error('Redis does not support SQL queries');
  }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Database interface defines a contract for databases that can connect, disconnect, and execute SQL queries. The SqliteDatabase and RedisDatabase classes implement the Database interface, providing their own implementations of the methods.

By using interfaces and the ISP, we can create specific interfaces for each client's needs, rather than creating a monolithic interface that includes methods for all clients. This promotes modularity and maintainability, making the code more flexible and reusable.




Fifth :Dependency Inversion Principle (DIP)

The DIP states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. In other words, you should design your system so that high-level modules can be easily swapped out with other modules that implement the same abstractions. This principle helps to promote extensibility and maintainability.
In TypeScript, you can use inversion of control (IoC) and dependency injection (DI) to achieve the DIP.

Here's an example:

// Logger.ts
export interface Logger {
  log(message: string): void;
}

// ConsoleLogger.ts
import { Logger } from './Logger';

export class ConsoleLogger implements Logger {
  public log(message: string): void {
    console.log(message);
  }
}

// Service.ts
import { Logger } from './Logger';

export class Service {
  private logger: Logger;

  constructor(logger: Logger) {
    this.logger = logger;
  }

  public doSomething(): void {
    this.logger.log('Doing something');
  }
}

Enter fullscreen mode Exit fullscreen mode

In this example, the Logger interface defines a contract for loggers that can log messages. The ConsoleLogger class implements the Logger interface, providing its own implementation of the log() method. The Service class depends on the Logger interface, rather than depending on a specific logger implementation. This allows us to easily swap out the ConsoleLogger with another logger implementation that implements the same Logger interface.

By using IoC and DI, we can design our system so that high-level modules depend on abstractions rather than low-level modules, promoting extensibility and maintainability.




So .. Who Wins?

TypeScript has enhanced OOP capabilities than JavaScript, particularly when it comes to adhering to the SOLID principles.

By adhering to these principles, you can create code that is more modular, maintainable, and robust.

In addition to adhering to the SOLID principles, TypeScript also provides several key features that make it a powerful tool for building complex software systems. These features make TypeScript a powerful tool for building complex software systems, particularly those that rely heavily on OOP principles.

By using TypeScript, you can create code that is more reliable, maintainable, and extensible, which can save you time and money in the long run. Moreover, TypeScript provides a powerful set of tools for creating and managing complex software systems, making it an excellent choice for building large-scale applications.




References

Feel free to check out the references for more details and points of view

Top comments (0)