Table of contents
- WHAT IS A CLASS?
- WHAT TYPESCRIPT ADDS?
- SYNTAX, DECLARATION & INSTANCE CREATION:
- constructor FUNCTION & CLASS PROPERTIES:
- CLASS METHODS & this KEYWORD:
- INHERITANCE:
- ENCAPSULATION & ACCESS MODIFIERS:
- SHORTHAND INITIALIZATION:
- GETTERS & SETTERS:
- STATIC PROPERTIES & METHODS:
- ABSTRACTION & ABSTRACT CLASS:
- SINGLETON PATTERN:
- CONCLUSION:
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.
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
.:
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:
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:
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();
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
.
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.
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:
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:
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:
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:
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:
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:
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:
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.
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, andprotected
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.
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:
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:
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 ofMath
arestatic
.
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 theabstract
keyword for methods to declareabstract 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:
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:
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:
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:
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!