DEV Community

Cover image for What about OOP in Typescript πŸ€”A quick overview
Nada Ezzat
Nada Ezzat

Posted on

What about OOP in Typescript πŸ€”A quick overview

Table of contents


Introduction

Hello devs πŸ˜„

As many of us know TypeScript is a superset of JavaScript that adds typing, classes, and interfaces to the language. These features make TypeScript a powerful language for OOP

TypeScript really excels when it comes to oop with JavaScript as It makes programming in an object-oriented fashion appear much like it does in other object-oriented languages such as C# or Java, .So devs who are familiar with those languages will easily and quickly learn typescript
For example:

Private access modifier in JS

class Base {
  #x = 0;
  constructor() {}
}

const b = new Base();
console.log(b.x); // this line will log undefined
Enter fullscreen mode Exit fullscreen mode

Private access modifier in TS

class Base {
  private x = 0; // Looks like C++ or Java Right πŸ˜„
  constructor() {}
}

const b = new Base();
console.log(b.x); //  this line will raise an error
Enter fullscreen mode Exit fullscreen mode

Property x is private and only accessible within class Base

So let's start with the 4 pillars of OOP in TSπŸ˜„

OOP Pillars

Inheritance

Inheritance is the ability to create new classes based on existing ones. In TypeScript, you can use the "extends" keyword to create a subclass that inherits properties and methods from a parent class. Here are some examples of inheritance in TypeScript:

Simple Inheritance

class Animal {
  name: string;

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

  makeSound() {
    console.log("Generic animal sound");
  }
}

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

const myDog = new Dog("Buddy");

console.log(myDog.name); // Output: Buddy

myDog.makeSound(); // Output: Woo!
Enter fullscreen mode Exit fullscreen mode

Multiple Inheritance

  • In TypeScript, we can’t inherit or extend from more than one class, but Mixins helps us to get around that.

  • Mixins create partial classes that we can combine to form a single class that contains all the methods and properties from the partial classes you can check documentation thus here i am trying to focus on main and basic concepts of OOP .

Encapsulation πŸ“¬ πŸ“«

Encapsulation is the way of hiding the internal details of an object and exposing only what is necessary and what you specify . In TypeScript, you can use access modifiers like public, private, and protected to control access to class members.
Here are some examples of encapsulation in TypeScript:

Example 1: Private & public Access Modifiers

class Person {
  private name: string;

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

  public greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const person = new Person("John");
person.greet(); // Output: Hello, my name is John as
console.log(person.name); // Error
Enter fullscreen mode Exit fullscreen mode
  • greet method is a public method can be accessed anywhere.
  • Property name is private so it isn't accessible outside the class.

Example 2: Protected Access Modifier

class Animal {
  protected name: string;

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

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

  public bark() {
    console.log(`Woo, my name is ${this.name}.`);
  }
}

const dog = new Dog("Buddy");
dog.bark(); // Output: Woo, my name is Buddy.
console.log(dog.name); // Error:
Enter fullscreen mode Exit fullscreen mode
  • bark( ) method can access the protected member name as it is inside the child class dog
  • Property name is protected and only accessible within class Animal and its subclasses

Abstraction

Abstraction identifies only the required characteristics of an object while ignoring how to implement details. In TypeScript, you can use abstract classes and Interfaces to implement abstraction concept.


Abstract classes

abstract class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
  abstract makeSound(): void;
  move(distanceInMeters: number) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
  makeSound() {
    console.log("Woo!");
  }
}

const myDog = new Dog("Buddy");
console.log(myDog.name); // Output: Buddy
myDog.move(10); // Output: Animal moved 10m.
myDog.makeSound(); // Output: Woo!
Enter fullscreen mode Exit fullscreen mode

πŸ“ Note:

  • The class which implements an abstract class must call super( ) in the constructor.
  • you have to implement abstract methods like makeSound that are extended from abstract classes

Interfaces

Generally, Interfaces serve as a contract in code. it’s all or nothing. When you implement an interface, you must implement everything defined in that interface methods to implement abstraction. Here's an example:

interface Animal {
  name: string;
  makeSound(): void;
}

class Dog implements Animal {
  name: string;

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

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

const myDog = new Dog("Buddy");
console.log(myDog.name); // Output: Buddy
myDog.makeSound(); // Output: Woo!
Enter fullscreen mode Exit fullscreen mode
  • ⚠️ In case we missed to implement whether the property name or method makeSound( ) we will get an error

polymorphism

Polymorphism is the ability of method to have different forms, whether toy are backend of frontend you are going to use Polymorphism may be

  • In Database Access: For example, a SQL database and a NoSQL database can both implement a common database interface and be used in the same way. This makes it easy to switch between different types of databases without changing the code that accesses the database.

  • Polymorphism can be represented by Overriding or Overloading.

Overriding

class Animal {
  name: string;

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

  makeSound() {
    console.log("Generic animal sound.");
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
  makeSound() {
    console.log("Woo!");
  }
}

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

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

const myDog = new Dog("Buddy");
const myCat = new Cat("Whiskers");

makeAnimalSound(myDog); // Output: Woo!
makeAnimalSound(myCat); // Output: Meow!
Enter fullscreen mode Exit fullscreen mode

Base class Animal has makeSound( ) method, subclasses Cat and Dog extend Animal have same method but with the overridden version

Overloading

Function Overloading with a different number of parameters and different types along with the same function name is not supported in normal way like :

class Greeter {
  message: string;

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

  // Overload signatures
  greet(personName: string): string {
    return `${this.message}, ${personName}!`;
  } // Duplicate function implementation error

  greet(persons: string[]): string[] {
    return persons.map((personName) => `${this.message}, ${personName}!`);
  } // Duplicate function implementation error
}
Enter fullscreen mode Exit fullscreen mode

I think As plain javascript couldn't differ between the overloaded methods as it doesn't support types, we have to do some work manually to check types of parameters in implementation.

So How can we implement overloading ?

  1. By defining a few overload signatures and one implementation signature.
class Greeter {
  message: string;

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

  // Overload signatures
  greet(person: string): string;
  greet(persons: string[]): string[];
}
Enter fullscreen mode Exit fullscreen mode
  1. The implementation signature has more generic parameter and return types and implements functionality.

Then our class become:

class Greeter {
  message: string;

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

  // Overload signatures
  greet(person: string): string;
  greet(persons: string[]): string[];

  // Implementation signature
  greet(person: unknown): unknown {
    if (typeof person === "string") {
      return `${this.message}, ${person}!`;
    } else if (Array.isArray(person)) {
      return person.map((name) => `${this.message}, ${name}!`);
    }
    throw new Error("Unable to greet");
  }
}

const hi = new Greeter("Hi");

hi.greet("Angela"); // Output 'Hi, Angela!'
hi.greet(["Pam", "Jim"]); // Output ['Hi, Pam!', 'Hi, Jim!']
Enter fullscreen mode Exit fullscreen mode

πŸ“ Note:
implementation signature is not callable. Only the overload signatures are callable.

greet("World"); // Overload signature is callable
greet(["Jane", "Joe"]); // Overload signature is callable

const someValue: unknown = "Unknown";
greet(someValue); // Error as implementation signature is NOT callable
Enter fullscreen mode Exit fullscreen mode

Thank you for reading i hope you enjoyed and I hope this will help someone better understand Object Oriented Programming with TypeScript πŸ˜„

I will be happy if you share with me your comments and knowledge 😁

References

Top comments (0)