Mastering Object-Oriented Programming with TypeScript: Encapsulation, Abstraction, Inheritance, and Polymorphism Explained
Tags: typescript, javascript, beginners, programming
Published: 2023-07-21 16:22:54

## What is OOPS?

Object-Oriented Programming is a programming paradigm that revolves around the concept of "objects." An object is a self-contained unit that encapsulates data (attributes) and behaviors (methods) that operate on that data. The primary principles of OOP include:

1. **Encapsulation:** Encapsulation refers to the bundling of data (attributes) and the methods that operate on that data within a single unit (object). It hides the internal implementation details of the object, allowing the object to be treated as a black box, and only exposing a public interface through which other parts of the code can interact with it.

2. **Abstraction:** Abstraction allows you to focus on the essential aspects of an object while hiding the unnecessary complexities. It helps in creating a clear separation between the interface (how an object is used) and the implementation (how it is internally defined).

3. **Inheritance:** Inheritance is a mechanism that enables a class (subclass) to inherit the properties and methods of another class (superclass). This promotes code reusability and hierarchical relationships between classes.

4. **Polymorphism:** Polymorphism allows objects of different classes to be treated as objects of a common superclass. It enables a single interface to represent different types of objects, providing flexibility and extensibility to the codebase.

In TypeScript, you can create classes and interfaces to implement object-oriented programming concepts. Here's a basic example of a TypeScript class:

```typescript
class Animal {
  private name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  makeSound() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
  
  makeSound() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy");
dog.makeSound(); // Output: "Buddy barks."
```

In this example, we define an `Animal` class with a `makeSound` method, and then a `Dog` class that extends `Animal`. The `Dog` class overrides the `makeSound` method to provide a specialized implementation.

By using OOP principles in TypeScript, you can create more organized, maintainable, and scalable applications.

## What is the class and obeject?
 
A class is a blueprint for creating objects. It defines the structure and behavior of objects that will be instantiated based on the class. A class in TypeScript contains properties (also known as fields or attributes) and methods (functions associated with the class). It encapsulates the data and operations that are related to a particular concept or entity.

Here's the basic syntax for creating a class in TypeScript:

```typescript
class ClassName {
  // Properties (attributes)
  propertyName1: type;
  propertyName2: type;

  // Constructor
  constructor(parameter1: type, parameter2: type) {
    this.propertyName1 = parameter1;
    this.propertyName2 = parameter2;
  }

  // Methods
  methodName1() {
    // Method implementation
  }

  methodName2() {
    // Method implementation
  }
}
```

Let's create a simple class in TypeScript representing a basic car:

```typescript
class Car {
  // Properties
  make: string;
  model: string;
  year: number;

  // Constructor
  constructor(make: string, model: string, year: number) {
    this.make = make;
    this.model = model;
    this.year = year;
  }

  // Method
  start() {
    console.log(`Starting the ${this.make} ${this.model}.`);
  }
}
```

Now, an object is an instance of a class. Once you have defined a class, you can create objects based on that class blueprint. Objects represent specific instances of the entity described by the class. Each object has its own set of properties and can invoke methods defined in the class.

Here's how you create an object based on the `Car` class we defined earlier:

```typescript
const myCar = new Car("Toyota", "Camry", 2022);
const anotherCar = new Car("Honda", "Accord", 2023);

myCar.start(); // Output: "Starting the Toyota Camry."
anotherCar.start(); // Output: "Starting the Honda Accord."
```

In this example, we created two separate instances of the `Car` class, `myCar` and `anotherCar`, each with its own set of properties (`make`, `model`, and `year`) and the ability to call the `start` method.

Using classes and objects in TypeScript allows you to structure your code more efficiently, promote code reuse through inheritance, and take advantage of object-oriented programming principles.

## Encapsulation

Encapsulation is one of the fundamental principles of Object-Oriented Programming (OOP). It refers to the bundling of data (attributes) and the methods (functions) that operate on that data within a single unit, known as a class. The main goal of encapsulation is to hide the internal implementation details of an object and only expose a well-defined public interface through which other parts of the code can interact with the object. This concept is often summarized with the phrase "data hiding."

Encapsulation provides several benefits:

1. **Modularity:** By encapsulating related data and behavior within a class, you create a self-contained module that can be reused and maintained independently.

2. **Data Protection:** By making the internal data private (or protected), you prevent unauthorized access and modification from outside the class, ensuring data integrity and security.

3. **Code Flexibility:** Encapsulation allows you to change the internal implementation of a class without affecting the code that uses the class, as long as the public interface remains unchanged.

Now, let's see an example of encapsulation in TypeScript:

```typescript
class BankAccount {
  private accountNumber: string;
  private balance: number;

  constructor(accountNumber: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
  }

  public getAccountNumber(): string {
    return this.accountNumber;
  }

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

  public deposit(amount: number): void {
    this.balance += amount;
    console.log(`Deposited ${amount}. New balance: ${this.balance}`);
  }

  public withdraw(amount: number): void {
    if (this.balance >= amount) {
      this.balance -= amount;
      console.log(`Withdrawn ${amount}. New balance: ${this.balance}`);
    } else {
      console.log("Insufficient balance");
    }
  }
}
```

In this example, we have a `BankAccount` class that represents a simple bank account. The class has two private properties: `accountNumber` and `balance`. These properties are marked as private, so they are not accessible from outside the class. This ensures that other parts of the code cannot directly access or modify these properties.

To interact with the private properties, the class provides public methods: `getAccountNumber`, `getBalance`, `deposit`, and `withdraw`. These methods serve as the public interface through which other parts of the code can interact with the `BankAccount` object.

Now, let's use this class:

```typescript
const account = new BankAccount("123456789", 1000);

console.log("Account Number:", account.getAccountNumber()); // Output: "Account Number: 123456789"
console.log("Balance:", account.getBalance()); // Output: "Balance: 1000"

account.deposit(500); // Output: "Deposited 500. New balance: 1500"
account.withdraw(200); // Output: "Withdrawn 200. New balance: 1300"
account.withdraw(1500); // Output: "Insufficient balance"
```

In this usage example, we can see that we can access the account number and balance through the public methods, but we cannot directly modify them. This is due to the encapsulation, which protects the internal state of the `BankAccount` object and allows controlled access through the defined public interface.

By encapsulating the data within the class and providing well-defined methods to interact with it, we ensure that the state of the object remains consistent and secure, making the code easier to maintain and less prone to bugs or accidental misuse.

## Abstraction

Abstraction is a key concept in Object-Oriented Programming (OOP) that focuses on presenting essential features of an object while hiding unnecessary details. It allows you to represent complex systems or entities in a simplified manner, making it easier to understand and work with them. Abstraction enables you to build models that capture the relevant characteristics of an object, without exposing all the implementation specifics.

In OOP, abstraction is achieved through the use of abstract classes and interfaces. These abstract constructs provide a blueprint for other classes to follow, defining a set of methods and properties without providing their implementation. The concrete subclasses that extend the abstract class or implement the interface are responsible for implementing these abstract elements.

The main benefits of abstraction are:

1. **Simplified Complexity:** Abstraction allows you to focus on the high-level design and behavior of an object, hiding the intricate details that might not be relevant at that level.

2. **Code Reusability:** By defining common interfaces through abstract classes or interfaces, you promote code reuse. Concrete classes can inherit from an abstract class or implement an interface, inheriting its structure and behavior.

3. **Flexibility:** Abstraction enables you to change the implementation details of concrete classes without affecting the overall functionality of the program, as long as the abstract interface remains unchanged.

Let's illustrate abstraction with an example:

```typescript
// Abstract class representing a shape
abstract class Shape {
  abstract getArea(): number;
  abstract getPerimeter(): number;
}

// Concrete subclass representing a Circle
class Circle extends Shape {
  private radius: number;

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

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

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

// Concrete subclass representing a Rectangle
class Rectangle extends Shape {
  private width: number;
  private height: number;

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

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

  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
}
```

In this example, we have an abstract class called `Shape`. It defines two abstract methods: `getArea()` and `getPerimeter()`. These methods represent the essential characteristics of any shape, but they are not implemented in the `Shape` class itself.

Then, we have two concrete subclasses, `Circle` and `Rectangle`, that extend the `Shape` class. These subclasses are responsible for providing implementations for the abstract methods.

By using abstraction, we can create a collection of different shapes and calculate their areas and perimeters without worrying about the specific implementation details of each shape:

```typescript
function printShapeDetails(shape: Shape) {
  console.log("Area:", shape.getArea());
  console.log("Perimeter:", shape.getPerimeter());
}

const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);

printShapeDetails(circle);
printShapeDetails(rectangle);
```

When we run this code, we get the following output:

```
Area: 78.53981633974483
Perimeter: 31.41592653589793
Area: 24
Perimeter: 20
```

The beauty of abstraction is that the `printShapeDetails` function can work with any shape that implements the `Shape` interface. We can create new shapes, such as triangles, squares, etc., without modifying the `Shape` class or the `printShapeDetails` function. This demonstrates the flexibility and reusability achieved through abstraction.

## Inheritance

Inheritance is a core concept in Object-Oriented Programming (OOP) that allows a class (subclass) to inherit properties and methods from another class (superclass). It forms an "is-a" relationship between classes, where the subclass is a specialized version of the superclass. Inheritance promotes code reuse, as it allows you to define common attributes and behaviors in a superclass and then extend or modify them in subclasses.

The class that is being inherited from is called the superclass or base class, while the class that inherits from the superclass is called the subclass or derived class.

Here are the main advantages of inheritance:

1. **Code Reusability:** Inheritance allows you to reuse the functionality defined in the superclass, reducing the amount of redundant code in your application.

2. **Modularity:** Inheritance promotes a hierarchical organization of classes, making the code more organized and easier to maintain.

3. **Polymorphism:** Inherited methods can be overridden in the subclass to provide specialized behavior, allowing for polymorphic behavior when dealing with objects of different classes through a common superclass interface.

Let's demonstrate inheritance with an example:

```typescript
// Base class: Animal
class Animal {
  private name: string;
  private age: number;

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

  makeSound() {
    console.log("Some generic sound");
  }

  getInfo() {
    return `Name: ${this.name}, Age: ${this.age}`;
  }
}

// Subclass: Dog (inherits from Animal)
class Dog extends Animal {
  private breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age);
    this.breed = breed;
  }

  makeSound() {
    console.log("Woof!");
  }

  getInfo() {
    return `${super.getInfo()}, Breed: ${this.breed}`;
  }
}
```

In this example, we have a base class `Animal` with properties `name` and `age`, and methods `makeSound()` and `getInfo()`. Then, we define a subclass `Dog` that inherits from `Animal`. The `Dog` class adds its specific property `breed` and overrides the `makeSound()` and `getInfo()` methods.

Now, let's create objects based on these classes:

```typescript
const genericAnimal = new Animal("Generic Animal", 5);
console.log(genericAnimal.getInfo()); // Output: "Name: Generic Animal, Age: 5"
genericAnimal.makeSound(); // Output: "Some generic sound"

const dog = new Dog("Buddy", 3, "Labrador");
console.log(dog.getInfo()); // Output: "Name: Buddy, Age: 3, Breed: Labrador"
dog.makeSound(); // Output: "Woof!"
```

In this example, we can see that the `Dog` class inherits the `name`, `age`, and methods from the `Animal` class. The `makeSound()` method is overridden in the `Dog` class to provide the specific sound of a dog ("Woof!"). Additionally, the `getInfo()` method is also overridden in the `Dog` class to include the `breed` property along with the `name` and `age`.

By using inheritance, we can create a hierarchy of classes where each subclass inherits and extends the functionality of the superclass. This approach allows us to reuse common code, define specialized behaviors, and build complex systems in a structured and modular way.

## Polymorphism

Polymorphism is another important concept in Object-Oriented Programming (OOP) that allows objects of different classes to be treated as objects of a common superclass. It enables a single interface (method or property) to represent different types of objects, providing flexibility and extensibility to the codebase. Polymorphism is often achieved through method overriding and method overloading.

There are two types of polymorphism:

1. **Compile-time Polymorphism (Static Polymorphism):** This type of polymorphism is resolved at compile time. It occurs when the method overloading is used, i.e., having multiple methods with the same name but different parameter lists. The compiler determines the appropriate method to call based on the method's signature and the arguments passed during the function call.

2. **Run-time Polymorphism (Dynamic Polymorphism):** This type of polymorphism is resolved at run time. It occurs when the method overriding is used, i.e., having a method in the subclass with the same name and signature as the one in the superclass. The method to be called is determined at run time based on the actual type of the object.

Let's illustrate both types of polymorphism with an example:

```typescript
// Compile-time Polymorphism (Method Overloading)
class MathOperations {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: any, b: any): any {
    return a + b;
  }
}

const math = new MathOperations();
console.log(math.add(5, 10)); // Output: 15 (number addition)
console.log(math.add("Hello, ", "World!")); // Output: "Hello, World!" (string concatenation)

// Run-time Polymorphism (Method Overriding)
class Animal {
  makeSound() {
    console.log("Some generic sound");
  }
}

class Dog extends Animal {
  makeSound() {
    console.log("Woof!");
  }
}

class Cat extends Animal {
  makeSound() {
    console.log("Meow!");
  }
}

function animalSound(animal: Animal) {
  animal.makeSound();
}

const dog = new Dog();
const cat = new Cat();

animalSound(dog); // Output: "Woof!" (Dog's sound)
animalSound(cat); // Output: "Meow!" (Cat's sound)
```

In this example, we first demonstrate compile-time polymorphism through method overloading in the `MathOperations` class. We define two versions of the `add` method: one for number addition and another for string concatenation. The appropriate method is chosen at compile time based on the argument types used during the function call.

Next, we demonstrate run-time polymorphism through method overriding in the `Animal`, `Dog`, and `Cat` classes. The `Animal` class has a `makeSound` method that provides a generic sound. Both the `Dog` and `Cat` classes override the `makeSound` method to provide their specific sounds. When we call the `animalSound` function with different objects of `Dog` and `Cat`, the appropriate `makeSound` method is dynamically determined at run time based on the actual object's type.

Polymorphism allows you to write more flexible and extensible code by treating objects based on their common interface rather than their specific types. It plays a crucial role in designing large-scale applications and simplifies the interactions between various classes and modules.

## TypeScript Interfaces

In TypeScript, interfaces are used to define the structure and shape of an object. They provide a way to define contracts that objects must adhere to, specifying the properties, methods, and their types that an object of that interface should have. Interfaces play a crucial role in achieving static type checking and providing code documentation. Here's an overview of TypeScript interfaces:

### Interface Declaration:
   You can declare an interface using the `interface` keyword, followed by the name of the interface. For example:

   ```typescript
   interface Person {
     name: string;
     age: number;
   }
   ```

### Properties:
   Interfaces define the properties that an object must have. Each property is defined with a name and its type. For example:

   ```typescript
   interface Person {
     name: string;
     age: number;
   }
   ```

### Optional Properties:
   You can make properties optional in an interface by adding a `?` (question mark) after the property name. These properties can be present or omitted in the implementing object. For example:

   ```typescript
   interface Person {
     name: string;
     age?: number;
   }
   ```
### Readonly Properties:
   You can mark properties as readonly using the `readonly` modifier. Readonly properties can only be assigned a value when the object is created and cannot be modified thereafter. For example:

   ```typescript
   interface Person {
     readonly name: string;
     readonly age: number;
   }
   ```

### Methods:
   Interfaces can also define methods that an object should implement. Method signatures include the method name, parameter types, and return type. For example:

   ```typescript
   interface Calculator {
     add(a: number, b: number): number;
     subtract(a: number, b: number): number;
   }
   ```

### Extending Interfaces:
   Interfaces can extend other interfaces, inheriting their properties and methods while adding new ones. This helps in creating modular and reusable interface definitions. For example:

   ```typescript
   interface Employee extends Person {
     employeeId: number;
     department: string;
   }
   ```

### Implementing Interfaces:
   To ensure an object adheres to an interface, you can use the `implements` keyword to specify that the object implements a particular interface. TypeScript will enforce that the object provides the required properties and methods. For example:

   ```typescript
   class Person implements Employee {
     name: string;
     age: number;
     employeeId: number;
     department: string;
   }
   ```

Interfaces in TypeScript provide a way to define contracts and enforce type checking for objects. They promote code reusability, maintainability, and help in catching errors at compile-time. They are a fundamental tool in writing type-safe and structured code in TypeScript.


---
🌟 **Thank You for Joining the Journey!** 🌟

>I hope you found this blog post informative and engaging. Your support means the world to me, and I'm thrilled to have you as part of my community. To stay updated on my latest content.


📌 **Follow me on Social Media!** 📌


🌐 [Visit my Website](https://raj-rathod.github.io/rajesh-rathore/)
📢 [Connect with me on Twitter](https://twitter.com/Rajesh946055)
📷 [Follow me on Instagram](https://www.instagram.com/raj_rathod1313/?hl=en)
📚 [Connect on LinkedIn](https://www.linkedin.com/in/rajesh-rathore-0501/)
📌 [Check out my GitHub](https://github.com/raj-rathod)

💌 **A Special Message to You!** 💌

To all my dedicated readers and fellow tech enthusiasts, I want to express my gratitude for your continuous support. Your engagement, comments, and feedback mean the world to me. Let's keep learning, growing, and sharing our passion for development!

👥 Let's Stay Connected! 👥
If you enjoy my content and want to stay in the loop with my latest posts, please consider following me on my social media platforms. Your support is invaluable.

Thank you for being a part of this amazing journey! 🚀

---