TypeScript Functions

 

Understanding Classes and Inheritance in TypeScript

TypeScript, a superset of JavaScript, brings static typing and enhanced tooling to the world of web development. One of the most powerful features of TypeScript is its ability to leverage object-oriented programming (OOP) principles, including classes and inheritance. If you’re new to TypeScript or come from a JavaScript background, understanding classes and inheritance is essential for writing maintainable, scalable, and organized code. In this blog post, we’ll delve into the concepts of classes and inheritance in TypeScript, providing code examples to help you grasp these fundamental concepts.

Understanding Classes and Inheritance in TypeScript

1. What are Classes?

In object-oriented programming, a class is a blueprint for creating objects. It defines a set of properties and methods that the objects instantiated from it will possess. Classes serve as a template or a model, encapsulating data and behavior relevant to a particular entity. This approach promotes code reusability, modularity, and maintainability.

2. Defining Classes in TypeScript

In TypeScript, you can define a class using the class keyword followed by the class name and a pair of curly braces. Let’s create a simple class to represent a Person.

typescript
class Person {
  name: string;
  age: number;

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

In the above example, we’ve defined a Person class with two properties: name and age, and a method sayHello() that logs a greeting to the console. However, at this point, we haven’t instantiated any objects.

2.1 Properties

Class properties are variables that store data related to the class. They are similar to variables declared within functions but are part of the class instance and can be accessed through the this keyword. TypeScript allows specifying the type of each property to enforce type safety.

2.2 Methods

Methods are functions defined within a class that can perform actions or computations related to the class. Like properties, methods are also part of the class instance and have access to the class’s properties and other methods through the this keyword.

3. Constructors and the this Keyword

In the previous example, we defined a class with properties, but we need to set their initial values when we create an object. Constructors in TypeScript allow us to initialize the object’s state and perform other setup tasks when the object is created. A constructor is a special method with the name constructor.

Let’s update our Person class to include a constructor:

typescript
class Person {
  name: string;
  age: number;

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

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

Now we can create a Person object with specific values for name and age during instantiation:

typescript
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

person1.sayHello(); // Output: "Hello, my name is Alice and I am 30 years old."
person2.sayHello(); // Output: "Hello, my name is Bob and I am 25 years old."

The this keyword refers to the current instance of the class. Inside the constructor, this represents the object being created, allowing us to set its properties based on the provided arguments.

4. Access Modifiers

Access modifiers in TypeScript define the visibility and accessibility of class members (properties and methods). There are three main access modifiers:

  • public: The default access modifier. Members marked as public can be accessed from anywhere, including outside the class.
  • private: Members marked as private can only be accessed within the class they are defined in. They are not accessible from outside the class or its instances.
  • protected: Members marked as protected are similar to private, but they are also accessible in subclasses that extend the current class.

Let’s see access modifiers in action:

typescript
class Car {
  public make: string;
  private model: string;
  protected year: number;

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

  public displayCarInfo() {
    console.log(`Make: ${this.make}, Model: ${this.model}, Year: ${this.year}`);
  }

  private checkIfCarIsOld() {
    return this.year < 2010;
  }
}

In the Car class, we have three members: make, model, and year. make is marked as public, model as private, and year as protected.

4.1 public, private, and protected

Now, let’s create a subclass that extends the Car class and see how the access modifiers work:

typescript
class ElectricCar extends Car {
  private batteryCapacity: number;

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

  public displayElectricCarInfo() {
    console.log(`Make: ${this.make}, Model: ${this.model}, Year: ${this.year}, Battery Capacity: ${this.batteryCapacity} kWh`);
  }
}

In the ElectricCar class, we extend the Car class using the extends keyword. This means ElectricCar inherits all the properties and methods from the Car class, including the protected property year. Additionally, ElectricCar introduces a new property batteryCapacity, which is marked as private.

5. Inheritance in TypeScript

Inheritance is a fundamental concept in OOP, allowing one class (subclass) to inherit properties and methods from another class (superclass). TypeScript supports single inheritance, which means a subclass can only inherit from one superclass.

5.1 Extending a Class

As we’ve seen in the previous example, we extend a class using the extends keyword. The subclass can then use the super keyword to call the constructor of the superclass and set its own unique properties.

5.2 Overriding Methods

Subclasses can override methods from the superclass. This means that a subclass can provide its own implementation of a method instead of using the one defined in the superclass. To override a method, the method in the subclass must have the same name and signature as the one in the superclass.

5.3 Calling Superclass Methods

Even after overriding a method, you can call the superclass’s version of the method using the super keyword. This allows you to reuse the logic defined in the superclass and extend it in the subclass.

Let’s create a more practical example to understand inheritance better:

typescript
class Animal {
  protected species: string;

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

  makeSound() {
    console.log('Animal sound');
  }
}

class Dog extends Animal {
  private breed: string;

  constructor(breed: string) {
    super('Dog');
    this.breed = breed;
  }

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

  barkAtStrangers() {
    console.log('Barking at strangers!');
  }
}

In this example, we have an Animal superclass and a Dog subclass. The Dog class extends Animal, inheriting its species property and makeSound() method. However, the Dog class overrides the makeSound() method to provide its unique sound.

6. Abstract Classes

In some cases, you may want to define a class that should not be instantiated directly but should serve as a blueprint for other classes. TypeScript supports abstract classes to achieve this.

An abstract class is a class that cannot be instantiated on its own. It must be extended by another class, which then provides implementations for its abstract methods. Abstract methods are methods without a body; they are just method signatures.

Let’s create an abstract class Shape with an abstract method getArea():

typescript
abstract class Shape {
  abstract getArea(): number;
}

class Square extends Shape {
  private sideLength: number;

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

  getArea() {
    return this.sideLength ** 2;
  }
}

The Shape class is marked as abstract, so we cannot create instances of it directly. The Square class extends Shape and provides an implementation for the getArea() method, calculating the area of the square.

Abstract classes are useful when you want to define a common interface for a group of related classes while leaving specific implementations to the subclasses.

7. Using Interfaces with Classes

In TypeScript, interfaces can be used to define contracts for classes. A class can implement an interface, promising to provide the properties and methods defined in the interface. This way, we achieve a degree of code decoupling and enforce consistency across different classes.

Let’s define an interface PersonInterface and have our Person class implement it:

typescript
interface PersonInterface {
  name: string;
  age: number;
  sayHello(): void;
}

class Person implements PersonInterface {
  // Class implementation remains the same
}

Now, the Person class must adhere to the contract defined by the PersonInterface. If any required property or method is missing, TypeScript will throw an error.

Interfaces are valuable in scenarios where multiple classes should share a common structure or behavior but don’t necessarily need to be related through inheritance.

Conclusion

Understanding classes and inheritance is crucial for writing clean, organized, and maintainable code in TypeScript. Classes provide a blueprint for creating objects, encapsulating data and behavior. Inheritance allows classes to inherit properties and methods from other classes, promoting code reuse and hierarchy. With TypeScript’s support for access modifiers, abstract classes, and interfaces, you have a powerful set of tools to build robust, scalable applications. Embrace the world of object-oriented programming in TypeScript, and take your web development skills to new heights!

In this blog post, we’ve covered the essentials of classes and inheritance, explored access modifiers, abstract classes, and interfaces, and provided practical code examples to solidify your understanding. As you continue your TypeScript journey, keep experimenting with classes and inheritance to unlock the full potential of this powerful language. Happy coding!

Previously at
Flag Argentina
Argentina
time icon
GMT-3
Experienced software engineer with a passion for TypeScript and full-stack development. TypeScript advocate with extensive 5 years experience spanning startups to global brands.