Classes in TypeScript

Classes in TypeScript

Looking back to our last article, we covered Functions in TypeScript which gave us an idea about how Functions work, are structured and are implemented in TypeScript.

In this article, we will discuss the syntax of creating classes, the different features available, how classes are treated during compile-time type-check, access modifiers, shorthand initialization, how Getters and Setters work, static properties/methods, abstract class, and Singleton pattern.

class.png

WHAT IS A CLASS?

Classes are a common abstraction used in object-oriented programming (OOP) languages to describe data structures known as objects. These objects may contain an initial state and implement behaviors bound to that particular object instance.

WHAT TYPESCRIPT ADDS?

In 2015, ECMAScript 6 introduced a new syntax to JavaScript to create classes that internally use the prototype features of the language.

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, access modifiers, and a few others.

SYNTAX, DECLARATION & INSTANCE CREATION:

The syntax is mostly the same used to create classes with JavaScript. But there are some distinguishing features available in TypeScript.

CREATE A CLASS:

You can create a class declaration by using the class keyword, followed by the class name and then a {} pair block, as shown in the following code:

class Course {

}

CREATE AN INSTANCE USING new KEYWORD:

The following snippet creates a new class named Course. You can then create a new instance of the Course class using the new keyword followed by the name of your class and an empty parameter list, as shown below:

class Course {

}

const courseInstance = new Course();

You can think of the Course class as a blueprint for creating objects and the courseInstance as one of those objects.

TYPE OF THE CREATED INSTANCE:

As you hover over the courseInstance variable, which is an instance of the class Course, you will see that TypeScript has inferred the type of the variable as Course.:

image.png

So, the class can also be considered as a type in TypeScript.

constructor FUNCTION & CLASS PROPERTIES:

Most of the time, when working with classes, you will need to create a constructor function. A constructor is a method that runs every time a new instance of a class is created (Not when you declare a class). It can be used to initialize values/fields in the class.

A class can hold variables called properties which can be initialized inside a constructor function as follows:

class Course {
  title: string;

  constructor(n: string) {
    this.title = n;
  }
}

Here, we are declaring a title variable/property of type string and initialize it within the constructor function.

When the constructor accepts a parameter, we need to pass it when creating an instance. The compilation error we will receive if we don't pass is as follows:

image.png

The Course class is accepting a variable of type string which is mandatory. Hence, it must be passed when creating an instance as follows:

class Course {
  title: string;

  constructor(n: string) {
    this.title = n;
  }
}

const courseInstance = new Course("Typescript");

Now that courseInstance represents an instance of the class Course, you can access fields, methods, and properties of the class Course via this variable as follows:

class Course {
  title: string;

  constructor(n: string) {
    this.title = n;
  }
}

const courseInstance = new Course("Typescript");

console.log(courseInstance.title); // This will print Typescript

CLASS METHODS & this KEYWORD:

Similar to properties, the class also can hold methods that are nothing but functions declared inside a class.

Let's take an example to understand the method:

class Course {
  title: string;

  constructor(n: string) {
    this.title = n;
  }

  describe() {
    console.log(`This is a ${this.title} course`);
  }
}

In the above code snippet, we are declaring a describe method inside our Course class which is not accepting any parameters.

Inside it, we are printing a string This is a ${this.title} course. Here, the this keyword refers to the concrete instance of Course (courseInstance). If you only specified title then it will throw a compilation error as follows:

image.png

Thus, if you want to access any property or method of the Course class, you have to use the this keyword.

Now, let's call describe and see what we get in the console.

class Course {
  title: string;

  constructor(n: string) {
    this.title = n;
  }

  describe() {
    console.log(`This is a ${this.title} course`);
  }
}

const courseInstance = new Course("TypeScript");

courseInstance.describe();

image.png

INHERITANCE:

WHAT IS INHERITANCE?

  • Inheritance is an aspect of OOPs languages, which provides the ability of a program to create a new class from an existing class. It is a mechanism that acquires the properties and behaviors of a class from another class.

  • The class whose members are inherited is called the base class, and the class that inherits those members is called the child class.

extends KEYWORD:

To implement inheritance we have to use the extends keyword as follows:

class Animal {

}

class Dog extends Animal {

}

In the above code snippet, we are declaring a parent class Animal and a child class Dog. Using the extends keyword we are inheriting properties and methods of an Animal class inside a Dog class.

super KEYWORD:

When you inherit any class, you must call the super method inside the constructor of the child class. super invokes the parent constructor and its values/parameters.

Let's take an example to understand this:

class Animal {
  name: string;

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

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

const dog = new Dog("Tuffy");

A parent class Animal has a name property and its constructor also expects a parameter named n of type string.

Since the Dog class inherits the parent class Animal, whenever we create an instance of this class we must call the parent class constructor function. This is done by calling super(dogName), which invokes the constructor of the Animal class with dogName as a parameter that the constructor of the Animal class expects.

A compilation error will occur if we do not pass any value inside super() because the Animal class expects a mandatory parameter n of type string.

image.png

In addition, since the Animal class expects a string type, we must pass string only if we pass a number we will get the following compilation error.

image.png

In short, whenever you inherit any class you have to call super to invoke its constructor.

ENCAPSULATION & ACCESS MODIFIERS:

WHAT IS ENCAPSULATION?

  • Encapsulation enables you to perform what’s called “data hiding”. It’s necessary to hide certain data so that it’s not changed accidentally or purposefully by other components or code in the program.

  • To achieve encapsulation, use the proper access modifiers combined with the proper member types to limit or expose the scope of data.

  • Access modifiers are markers on code that designate where that code can be used, and are as follows:

public MODIFIER:

public fields do not encapsulate since any calling code can modify the data at any time. If you declare a property/method without an access modifier, it is a public property/method.

Basically, public members are accessible everywhere without restrictions.

Let's take an example to understand more:

class Course {
  public title: string; 

  constructor(n: string) {
    this.title = n;
  }
}

const course = new Course('Angular');

console.log(course.title); // Prints: Angular

All TypeScript members (properties and methods) are public by default, so you don't need to prefix them with the public keyword.

You can also modify the values of the public properties. Look at the following code:

class Course {
  public title: string; 

  constructor(n: string) {
    this.title = n;
  }
}

const Course1 = new Course("Angular");

Course1.title = "TypeScript";

console.log(Course1.title); // Prints: TypeScript

private MODIFIER:

A private property/method cannot be accessed outside of its containing class. private properties and methods can only be accessed within the class.

Let's take an example to understand more:

class Course {
  private title: string; 

  constructor(n: string) {
    this.title = n;
  }
}

const course = new Course('Angular');

console.log(course.title); // Throws compilation error

The above code has a title property which is a private property of the class Course.

Accessing it outside of the Course class will result in the following compilation error:

image.png

The child class does not even have access to private properties. Consider the following code:

class Course {
  private title: string;

  constructor(n: string) {
    this.title = n;
  }
}

class PaidCourse extends Course {
  constructor() {
    super("Paid Course");
    console.log(this.title);
  }
}

In the above code, we have a Course class as a parent and a PaidCourse class as a child. We learned earlier that properties can be inherited from the parent class to the child class except for private properties. So the above code will result in the following compilation error:

image.png

protected MODIFIER:

A protected member cannot be accessed outside of its containing class. Members that are protected can only be accessed within the class and its child classes.

In the above code, if we change the title property of the Course class from private to protected, the compilation error will be resolved. Check out the following image:

image.png

You will, however, receive a compilation error if you try to access the protected title property through an instance of the Course class. See the following code:

class Course {
  protected title: string; // only accessible within the class and child classes

  constructor(n: string) {
    this.title = n;
  }
}

class PaidCourse extends Course {
  constructor() {
    super("Paid Course");
    console.log(this.title); // prints "Paid Course" (valid line)
  }
}

const course = new Course("Another course"); 

console.log(course.title); // throws error

Because the title is a protected property, we cannot access it through the instance. We will receive the following compilation error:

image.png

readonly MODIFIER:

TypeScript supports readonly modifiers on the property level by using the readonly keyword. The readonly properties must be initialized at their declaration or in the constructor.

Take a look at the following code to understand this:

class Course {
  readonly price: number;
  constructor(p: number) {
    this.price = p;
  }

  changePrice(p: number) {
    this.price = p; // throws error because price is readonly
  }
}

const Course1 = new Course(10);

Course1.changePrice(20);

We have a readonly price property in the above code, which can only be assigned once, inside the constructor function.

You will get a compilation error as follows if you try to change it anywhere else:

image.png

SHORTHAND INITIALIZATION:

In previous examples, we declared properties first and then initialized them inside the constructor as follows:

class Course {
  title: string;
  price: number;
  ratings: number;

  constructor(title: string, price: number, ratings: number) {
    this.title = title;
    this.price = price;
    this.ratings = ratings;
  }
}

The above code first declares three properties and sets/assigns them inside a constructor function.

It is possible to write less and more readable code by simplifying and using a shortcut, as follows:

class Course {
  constructor(
    public title: string,
    public price: number,
    public ratings: number
  ) {}
}

In this case, Typescript will automatically generate those properties. Based on your requirements, you can use any other access modifier instead of public.

Unless you use access modifiers, TypeScript will just consider it as a parameter and not generate a property.

For example, in the above code, if we use public only for title and amount, and pass ratings without any access modifier, it will throw an error if you attempt to access ratings the following way:

image.png

So you have to use an access modifier to make shorthand initialization valid.

GETTERS & SETTERS:

Getters and Setters are nothing more than methods that provide access to an object's properties.

They allow us to hide implementation details from instance objects. Thus, we can do some operations inside getters and setters that are completely encapsulated.

  • getter: Use this method when you want to access any property of an object. A getter is also called an accessor.

  • setter: Use this method when you want to change any property of an object. A setter is also known as a mutator.

A getter method starts with the keyword get and a setter method starts with the keyword set.

Let's look at Getters and Setters one by one with examples:

GETTER METHODS:

To understand the syntax and basic concept, let's take an example:

class Person {
  firstName: string;
  lastName: string;
  constructor(f_name: string, l_name: string) {
    this.firstName = f_name;
    this.lastName = l_name;
  }

  getFullName() {
    return this.firstName + " " + this.lastName;
  }
}

const person = new Person("John", "Doe");
console.log(person.getFullName()); // prints "John Doe"

In the above example, we have a class called Person that accepts f_name and l_name parameters and has firstName and lastName properties.

It also has a method called getFullName() that combines firstName and lastName to return a full name.

Instead of using the getFullName method, we can use getter as follows:

class Person {
  firstName: string;
  lastName: string;
  constructor(f_name: string, l_name: string) {
    this.firstName = f_name;
    this.lastName = l_name;
  }

  get fullName() {
    return this.firstName + " " + this.lastName;
  }
}

const person = new Person("John", "Doe");

// we dont execute getters
console.log(person.fullName); // prints "John Doe"

Using the get keyword, we declare a getter method. Notice that we don't execute or use () after person.fullName as TypeScript infers it as a readonly property. Hence, you cannot modify it. In doing so, you will receive the following error:

image.png

In the first example, the method getFullName does the same thing as the getter method. However, the getter method is slightly simpler and it is easier to identify its purpose at a glance with its syntax.

SETTER METHODS:

A setter method is used to mutate the value of a class member.

In our previous example, there was no way to replace a person's name other than to create a new object.

This provides us with class safety, but what if we need to change the firstName.

The setter method in TypeScript allows us to change the value of a property. (even if the value is private or protected)

To write a setter method, we use the keyword set before the method name.

class Person {
  private firstName: string;
  private lastName: string;
  constructor(f_name: string, l_name: string) {
    this.firstName = f_name;
    this.lastName = l_name;
  }

  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  set setFirstName(f_name: string) {
    this.firstName = f_name;
  }
}

const person = new Person("John", "Doe");

// Whatever value we assign to the setter, it will be passed to the setter as a parameter.
person.setFirstName = "Rohan"; // Setting the firstName property

console.log(person.fullName); // prints "Rohan Doe"

We have a class called Person with two private properties called firstName and lastName. Additionally, there is a getter method to retrieve a person's full name and a setter method to change the firstName.

IMPORTANT: Setter functions can take only one parameter. You will receive a compilation error if you try to pass more than one.

image.png

STATIC PROPERTIES & METHODS:

  • Static members can be accessed without having the class instantiated. Of course, it depends on which access modifier you are using.

  • So public static members can be accessed directly from outside the class.

  • private static members can only be used within the class, and

  • protected members can be accessed by the class in which the member is defined as well as by its child classes.

HOW TO ACCESS INSIDE A CLASS:

When working with the static properties/methods you have to use the class itself and not the instance of the class.

Check out the following code to see how to access them inside a class:

class Mathematics {
  static PI = 3.14159; // static property

  calculateCircumference(diameter: number): number {
    return this.PI * diameter; // this.PI is not accessible here because it is a static property
  }
}

Here, PI is a static property and calculateCircumference is a normal method in the Mathematics class.

Therefore, if you try to access it inside of a class method by using the this keyword, you will receive an error because PI is NOT a class property. Rather, it is a static property.

image.png

To remove the error, we need to access the static property using the class name:

class Mathematics {
  static PI = 3.14159; // static property

  calculateCircumference(diameter: number): number {
    return Mathematics.PI * diameter;
  }
}

NOTE: You can only use the this keyword inside a static method if you want to access the static property. Therefore, if you make calculateCircumference a static method then this.PI would be valid:

image.png

HOW TO ACCESS OUTSIDE A CLASS:

Check out the following code to see how to access them outside a class:

class Mathematics {
  static PI = 3.14159; // static property

  // static method
  static calculateCircumference(diameter: number): number {
    return Mathematics.PI * diameter;
  }
}

let newMath = new Mathematics();

console.log(newMath.PI); // This will throw an error
console.log(newMath.calculateCircumference(10)); // This will throw an error

console.log(Mathematics.PI); // returns 3.14159
console.log(Mathematics.calculateCircumference(10)); // returns 31.4159

If you try to access static property PI or static method calculateCircumference using newMath in the above code, you will get the following compilation error:

image.png

In addition, TypeScript also detects that the PI and calculateCircumference are static properties and methods respectively and asks "Did you mean to access the static member 'Mathematics.calculateCircumference' instead?".

INBUILT Math CLASS:

  • TypeScript's Math class provides no. of properties and methods for performing mathematical operations.

  • Math is not a constructor and all the properties and methods of Math are static.

function mathTest(num: number): void {
  var squareRoot = Math.sqrt(num); // sqrt is a static method of Math
  console.log("Random Number:  " + num); // logs random number
  console.log("Square root:  " + squareRoot); // logs Square root of random number
  console.log("PI value: ", Math.PI); // PI is a static property of Math
}
var randomNum: number = Math.random(); // random is a static method of Math
mathTest(randomNum);

ABSTRACTION & ABSTRACT CLASS:

  • Abstract classes can have implementation details for their members. To declare an abstract class, we can use the abstract keyword. We can also use the abstract keyword for methods to declare abstract methods, which are implemented by classes that derive from an abstract class.

  • In short abstract classes are classes that have a partial implementation of a class from which other classes can be derived. It's a blueprint for classes.

  • They can’t be instantiated directly.

SYNTAX AND ABSTRACT METHODS:

Let's take an example to understand the syntax and implementation:

abstract class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  abstract getName(): string;
  abstract getAge(): number;
}

In the above code, we have declared a Person class prefixed with the abstract keyword which means it is an abstract class.

We have name and age as properties for the Person class. It also has the abstract methods getName and getAge. But if you see these methods don't have implementations in them we have just declared them with their return type.

Because abstract methods don’t contain implementations of the method. It’s up to the child classes that inherit the abstract class to implement the method listed. They may also, optionally, include access modifiers.

INHERIT ABSTRACT CLASS:

Let's inherit the above class:

abstract class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  abstract getName(): string;
  abstract getAge(): number;
}


class Employee extends Person{
  constructor(name: string, age: number) {
    super(name, age);
  }
  getName() {
    return this.name;
  }
  getAge() {
    return this.age;
  }
}

As we can see, in the Person class the abstract methods only have signatures in them. The actual implementation of the methods is in the Employee class, which extends the Person class.

If you miss any one of the abstract methods inside an Employee class you will get a compilation error as follows:

image.png

TypeScript checks the method declaration and the return type of the method in the abstract class, so we must be implementing the abstract methods as it’s declared in the abstract class.

This means that in the example above, the getName method must take no parameters and must return a string. If you try to break those type checks you will get an error:

image.png

Likewise, the getAge method must take no parameters and must return a number.

After the abstract methods have been implemented, we can call them normally like any other method as follows:

let employee = new Employee("Jane", 20);
console.log(employee.getName()); // returns "Jane"
console.log(employee.getAge()); // returns 20

SINGLETON PATTERN:

  • Singleton is a creational design pattern, which ensures that only one object of its kind exists and provides a single point of access to it for any other code.

  • It is a way to structure your code so that you can’t have more than one instance of your logic, ever.

SINGLETON CLASS IN TYPESCRIPT:

We can use access modifiers on TypeScript constructors, so we can now create singletons as we do in other languages.

A singleton class's constructor is private, which means it cannot be used outside of the class. Therefore, we can't create an instance of that class with the new keyword.

To understand this let's create a singleton class:

class SingletonClass {
  private constructor() {
    console.log("SingletonClass created");
  }
}

const singletonClass = new SingletonClass(); // throws error

In the above code, we have a class called SingletonClass, which has a private constructor. So, when we try creating an instance from that class, we receive the following error:

image.png

The above error indicates that we can only access the constructor within the class itself.

To make it a singleton class, we need to create an instance within the class as follows:

class SingletonClass {
  private static instance: SingletonClass;

  // only accessible within the class
  private constructor() {
    console.log("SingletonClass created");
  }


  static getInstance() {
    if (SingletonClass.instance) {
      return SingletonClass.instance;
    }
    SingletonClass.instance = new SingletonClass();
    return SingletonClass.instance;
  }
}

const singletonClass = SingletonClass.getInstance(); // creates a new instance

The above code declares a private and static property named instance that is of type SingletonClass.

We have declared a static method getInstance that first checks if an instance of the class SingletonClass already exists or not. Upon success, it will return the same instance otherwise a new instance will be created.

Therefore, if you call getInstance multiple times, the constructor function will only be executed once, as shown in the following image:

image.png

USE CASES:

  • The purpose of the singleton class is to control object creation, limiting the number of objects to only one.

  • The singleton allows only one entry point to create a new instance of the class.

  • The use of singletons is often useful when we need to control resources, such as database connections or sockets.

CONCLUSION:

  • TypeScript classes are even more powerful than JavaScript classes because they have access to the type system and new features such as member visibility, access modifiers, abstract classes, and much more.

  • In this way, you can deliver code that is type-safe, more reliable, and more representative of your business model.

Make sure to subscribe to our newsletter on https://blog.wajeshubham.in/ and never miss any upcoming articles related to TypeScript and programming just like this one.

I hope this post will help you in your journey. Keep learning!

My Website, connect with me on LinkedIn and GitHub.