Does TypeScript Support All Object-Oriented Principles?


In the world of programming, object-oriented principles provide a solid foundation for designing and building software systems. Object-oriented programming (OOP) languages enable developers to model real-world entities as objects, encapsulate data and behavior, and establish relationships between objects. TypeScript, a superset of JavaScript, brings static typing to the language and offers many features to support object-oriented programming. In this tutorial, we will explore various scenarios to understand how well TypeScript aligns with the core principles of object-oriented programming.

Encapsulation

Encapsulation refers to the bundling of data and methods into a single unit, known as a class, and hiding the internal implementation details. TypeScript fully supports encapsulation through classes and access modifiers.

In the below example, the Car class has two properties (brand and year) marked with access modifiers. The private modifier restricts access to the brand property within the class, while the protected modifier allows subclasses to access the year property. The startEngine method is defined as public, making it accessible from outside the class. And we created an instance of the Car class and invoked the startEngine method.

class Car {
   private brand: string;
   protected year: number;    
   constructor(brand: string, year: number) {
      this.brand = brand;
      this.year = year;
   }    
   public startEngine() {
      console.log(`Starting the ${this.brand} engine...`);
   }
}
const myCar = new Car("Tesla", 2023);
myCar.startEngine();

On compliling, the above code will generate the following JavaScript code −

var Car = /** @class */ (function () {
   function Car(brand, year) {
      this.brand = brand;
      this.year = year;
   }
   Car.prototype.startEngine = function () {
      console.log("Starting the ".concat(this.brand, " engine..."));
   };
   return Car;
}());
var myCar = new Car("Tesla", 2023);
myCar.startEngine();

Output

Starting the Tesla engine...

In this example, we encapsulate the brand and year information within the Car class and expose the startEngine method for external use. The encapsulation principle ensures that the internal details of the class are hidden from the outside world, promoting modular and maintainable code.

Inheritance

Inheritance is a fundamental concept in object-oriented programming that allows a class to inherit properties and methods from another class. TypeScript provides robust support for inheritance, enabling developers to build hierarchical relationships between classes.

In this example, the Dog class extends the Animal class using the extends keyword. As a result, the Dog class inherits the name property from Animal and also defines its bark method. And we created an instance of the Dog class and invoked the move and bark methods.

class Animal {
   protected name: string;    
   constructor(name: string) {
      this.name = name;
   }    
   public move() {
      console.log(`${this.name} is moving...`);
   }
}
class Dog extends Animal {
   public bark() {
      console.log(`${this.name} is barking...`);
   }
}
const myDog = new Dog("Jimmy");
myDog.move();
myDog.bark();

On compliling, the above code will generate JavaScript Code.−

var __extends = (this && this.__extends) || (function () {
   var extendStatics = function (d, b) {
      extendStatics = Object.setPrototypeOf ||
         ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
         function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
      return extendStatics(d, b);
   };
   return function (d, b) {
      if (typeof b !== "function" && b !== null)
         throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
      extendStatics(d, b);
      function __() { this.constructor = d; }
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
   };
})();
var Animal = /** @class */ (function () {
   function Animal(name) {
      this.name = name;
   }
   Animal.prototype.move = function () {
      console.log("".concat(this.name, " is moving..."));
   };
   return Animal;
}());
var Dog = /** @class */ (function (_super) {
   __extends(Dog, _super);
   function Dog() {
      return _super !== null && _super.apply(this, arguments) || this;
   }
   Dog.prototype.bark = function () {
      console.log("".concat(this.name, " is barking..."));
   };
   return Dog;
}(Animal));
var myDog = new Dog("Jimmy");
myDog.move();
myDog.bark();

Output

On compliling, the above code will generate JavaScript Code. The JavaScript code will produce the following result −

Jummy is moving…
Jimmy is barking…

The inheritance feature in TypeScript allows us to create a hierarchy of classes, promoting code reuse and enabling the organization of related classes in a structured manner.

Polymorphism

Polymorphism is the ability of an object to take on multiple forms or behave differently based on the context. TypeScript supports polymorphism through method overriding and method overloading.

Method Overriding

Method overriding allows a subclass to override a method defined in its superclass. Here’s an example to demonstrate method overriding. In this example, we have an abstract class Shape that defines an abstract method calculateArea(). The Circle and Rectangle classes extend Shape and provide their implementations of the calculateArea() method. And we created instances of both Circle and Rectangle classes and calculated their respective areas.

abstract class Shape {
   protected color: string;    
   constructor(color: string) {
      this.color = color;
   }    
   public abstract calculateArea(): number;
}
class Circle extends Shape {
   private radius: number;    
   constructor(color: string, radius: number) {
      super(color);
      this.radius = radius;
   }    
   public calculateArea(): number {
      return Math.PI * this.radius * this.radius;
   }
}
class Rectangle extends Shape {
   private width: number;
   private height: number;    
   constructor(color: string, width: number, height: number) {
      super(color);
      this.width = width;
      this.height = height;
   }    
   public calculateArea(): number {
       return this.width * this.height;
   }
}
const myCircle = new Circle("red", 5);
const myRectangle = new Rectangle("blue", 4, 6);
console.log(myCircle.calculateArea()); 
console.log(myRectangle.calculateArea());

On compliling, the above code will generate JavaScript Code.−

var __extends = (this && this.__extends) || (function () {
   var extendStatics = function (d, b) {
      extendStatics = Object.setPrototypeOf ||
         ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
         function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
      return extendStatics(d, b);
   };
   return function (d, b) {
      if (typeof b !== "function" && b !== null)
         throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
      extendStatics(d, b);
      function __() { this.constructor = d; }
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
   };
})();
var Shape = /** @class */ (function () {
   function Shape(color) {
      this.color = color;
   }
   return Shape;
}());
var Circle = /** @class */ (function (_super) {
   __extends(Circle, _super);
   function Circle(color, radius) {
      var _this = _super.call(this, color) || this;
      _this.radius = radius;
      return _this;
   }
   Circle.prototype.calculateArea = function () {
      return Math.PI * this.radius * this.radius;
   };
   return Circle;
}(Shape));
var Rectangle = /** @class */ (function (_super) {
   __extends(Rectangle, _super);
   function Rectangle(color, width, height) {
      var _this = _super.call(this, color) || this;
      _this.width = width;
      _this.height = height;
      return _this;
   }
   Rectangle.prototype.calculateArea = function () {
      return this.width * this.height;
   };
   return Rectangle;
}(Shape));
var myCircle = new Circle("red", 5);
var myRectangle = new Rectangle("blue", 4, 6);
console.log(myCircle.calculateArea());
console.log(myRectangle.calculateArea());

Output

On compliling, the above code will generate JavaScript Code. The JavaScript code will produce the following result −

78.53981633974483
24

In this example, both the Circle and Rectangle classes inherit the calculateArea() method from the abstract Shape class. However, each subclass implements the method differently to calculate the area based on its specific shape. This showcases polymorphism, where objects of different classes can be treated as instances of a common superclass and exhibit different behaviors.

Method Overloading

Method overloading, on the other hand, enables us to define multiple methods with the same name but different parameters. TypeScript determines the appropriate method to invoke based on the number or types of arguments passed.

In the below code snippet, we have a Calculator class that defines two add methods—one for adding numbers and another for concatenating strings. And we created an instance of the Calculator class and invoked both add methods.

class Calculator {
   public add(a: string, b: string): string;
   public add(a: number, b: number): number;

   public add(a: any, b: any): any {
      return a + b;
   }
}

const myCalculator = new Calculator();
const sum = myCalculator.add(3, 5);
const concatenated = myCalculator.add("Hello, ", "TypeScript!");

console.log(sum);
console.log(concatenated);

On compliling, the above code will generate the follwoing JavaScript Code.

var Calculator = /** @class */ (function () {
   function Calculator() {
   }
   Calculator.prototype.add = function (a, b) {
      return a + b;
   };
   return Calculator;
}());
var myCalculator = new Calculator();
var sum = myCalculator.add(3, 5);
var concatenated = myCalculator.add("Hello, ", "TypeScript!");
console.log(sum);
console.log(concatenated);

Output

It will produce the following result −

8
Hello, TypeScript!

Abstraction

Abstraction allows us to define the essential features of an object while hiding the implementation details. TypeScript supports abstraction through abstract classes and methods. An abstract class serves as a blueprint for other classes and cannot be instantiated directly. It may contain abstract methods that derived classes must implement.

In this example, the Shape class is declared as abstract, and it defines an abstract method called a draw. The Circle class extends Shape and provides an implementation for the draw method. And we created an instance of the Circle class and invoked the draw method.

abstract class Shape {
   protected color: string;
    
   constructor(color: string) {
      this.color = color;
   }
    
   public abstract draw(): void;
}

class Circle extends Shape {
   private radius: number;
    
   constructor(color: string, radius: number) {
      super(color);
      this.radius = radius;
   }
    
   public draw() {
      console.log(`Drawing a ${this.color} circle with radius ${this.radius}`);
   }
}

const myCircle = new Circle("red", 5);
myCircle.draw();

On compliling, the above code will generate the follwoing JavaScript Code.

var __extends = (this && this.__extends) || (function () {
   var extendStatics = function (d, b) {
      extendStatics = Object.setPrototypeOf ||
         ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
         function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
      return extendStatics(d, b);
   };
   return function (d, b) {
      if (typeof b !== "function" && b !== null)
         throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
      extendStatics(d, b);
      function __() { this.constructor = d; }
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
   };
})();
var Shape = /** @class */ (function () {
   function Shape(color) {
      this.color = color;
   }
   return Shape;
}());
var Circle = /** @class */ (function (_super) {
   __extends(Circle, _super);
   function Circle(color, radius) {
      var _this = _super.call(this, color) || this;
      _this.radius = radius;
      return _this;
   }
   Circle.prototype.draw = function () {
      console.log("Drawing a ".concat(this.color, " circle with radius ").concat(this.radius));
   };
   return Circle;
}(Shape));
var myCircle = new Circle("red", 5);
myCircle.draw();

Output

On compliling, the above code will generate JavaScript Code. The JavaScript code will produce the following result −

Drawing a red circle with radius 5

The abstract class Shape provides a common interface (draw) that all derived classes must implement. This allows us to define generic behavior and enforce certain methods across different subclasses, promoting abstraction and ensuring consistency in the application's structure.

Conclusion

TypeScript offers robust support for object-oriented programming principles, including encapsulation, inheritance, polymorphism, and abstraction. By leveraging classes, access modifiers, inheritance, method overriding, method overloading, and abstract classes, developers can design and implement object-oriented systems in a more structured and maintainable manner. TypeScript's static typing capabilities further enhance the reliability and scalability of object-oriented codebases.

Updated on: 21-Aug-2023

128 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements