Introduction to class data type and Object-Oriented Programming paradigm in TypeScript

In this lesson, we are going to learn about classes, interfaces, and the basic concepts Object-Oriented Programming paradigm in TypeScript.

JavaScript supports the object-oriented paradigm using prototypal inheritance. In programming languages like Java, you are used to classical inheritance where one class inherits properties and methods of other classes, sadly that’s not how JavaScript works.

Until ES5, we had to use User.prototype.<method> expression to add a method on the User constructor function’s prototype so that objects created from that constructor function can inherit these methods.

💡 The story of prototypal inheritance is quite big and if you wanna learn about it, I have written this article just on this topic.

ES6 specifications came up with the class keyword to define a class and the extends keyword to inherit methods from another class. Though JavaScript is pretty much based on prototypal inheritance, it gives developers more time to focus on the productivity, and with this, OOP in JavaScript won’t be alien to those who work on the classical inheritance paradigm.

TypeScript follows the exact same syntax of classical inheritance as described in ES6 specification, plus it adds more functionality to the syntax to control the behavior of instances (objects) created from a class.

In the example above, we have created a Person class that has firstName and lastName field of type string and the age field of type number. These declarations are needed for the TypeScript compiler to understand the shape of the instance that will be generated from the Person class.

💡 The field and property word can be used interchangeably to indicate a field of the class or a property name on the object (instance). But in this article, a field is an entity in the class such as firstName property or getFullName method while a property is strictly a key inside an object (or instance) which is firstName in the ross object (instance). When we say class property, we mean a class field that is a property on the instance such as firstName.

The constructor method is necessary to create an instance of a class. When new keyword is used, this constructor is gets invoked which is primarily used to initialize the property values of the object. Your class can lack a constructor if you do not wish to initialize any properties on the instance.

The getFullName method lives on the prototype but it is accessible from the instance object, hence we can call it like ross.getFullName(). The this object inside a method of the class points to the instance object it was called on. Therefore this inside getFullName() is ross.

Default Property Values

A class property in a TypeScript class can have a default value. In the property declaration syntax, you can assign an initial value to a property that you want it to have before the constructor function is called. Hence, it is not necessary to have logic to assign a default value to a property from inside the constructor function.

In the above program, the age and type properties have a default value. Therefore we do not necessarily need to provide a type annotation for them as we did for the type property since TypeScriirpt will be infer them from their default (initial) value.

We have also made the age argument of the constructor function optional since we have the default value for age property in case it is not provided.

💡 You can entirely eliminate constructor function from a class if the class doesn’t have any properties or if all the properties have a default value.

Inheritance

The classical inheritance syntax makes inheritance much simple. In ES5, we had to use Object.create() to create a prototype object for the subclass that points to the prototype of the superclass as shown below.

function B(){ ... } // superclass\
function A(){ ... } // subclassA.prototype = Object.create( B.prototype);

Luckily we don’t have to do this anymore. When class A wants to inherit class B, we use class A extends B syntax. In this case, A is the subclass and B is the superclass. In this relationship, A has access to all the methods of B except the constructor, instance properties, or any static fields.

💡 You can not inherit more than one class at a time in JavaScript.

Since constructors of all classes needs to be called while creating an instance, we use super() call from inside the constructor of A with all the arguments that need to be provided for the constructor of B. This call invokes the constructor of B which might initialize values for some additional properties.

In the above example, Student class extends Person class, hence an instance of Student also has access to the getNameParts method. Since name property will be added by the constructor of the Person on the instance being created, we need to call super() with all the necessary arguments.

The super() call isn’t absolutely necessary unless the superclass has a constructor that dumps something into this. The super() call should be the first line in the constructor of the subclass.

💡 If the subclass doesn’ need a constructor, you do not need to create one just to call super(). If the subclass doesn’t have a constructor, JavaScript calls super() implicitly with all arguments passed in the new Student() call.

Method Overriding

In the inheritance pattern we saw above, if the subclass inherits the methods of superclass, what will happen when the subclass has the same method as the parent class (means their names are the same)?

In that case, the subclass method will be called since it is closer to the instance. This is called method overriding. However, the method from the parent class doesn’t get destroyed. It is still on the prototype chain and any method on the subclass can access it using the super keyword.

In the above example, getNameParts method exists in both the classes. But the ross.getNameParts() call executes the method of the Student class since ross is an instance of Student and this method is closer to the Student class than the method of the Person class.

However, super keyword points to the superclass and it contains references of the methods from the superclass (except the constructor). In the above example, we have made super.getNameParts() call from the getNameParts method of the Student and returned a transformed response.

💡 The overriding methods should have the exact same type signature else the TypeScript compiler will not allow method overriding. You might be familiar with the concept of method overloading in OOP, but it doesn’t exist in JavaScript or TypeScript. However there is a concept of function overloading in TypeScript.

Access Modifiers

JavaScript wasn’t designed to be used as a general-purpose programming language. Its main job was to provide some dynamic behavior to web applications such as AJAX and DOM manipulation. Hence it’s not very object-oriented and it provides a little or no data encapsulation.

For example, you might use public, private and protected access modifiers to control the access of the fields of a class in languages like Java or C++, but that certainly doesn’t exist in JavaScript. All properties of an object are public by default and can be accessed, modified, or deleted by anyone.

💡 Except for the newly introduced private field syntax (discussed below).

However, TypeScript does support these access modifiers. These do not change how JavaScript behaves but it adds access restrictions on the properties and methods during the compilation. Hence we won’t be able to compile the code if there are some issues with access to the fields of a class.

Public Fields

So far, we have created classes with fields that aren’t labeled public, private or protected. In that case, the TypeScript compiler assumes all fields and methods of the class are public implicitly.

The private access modifier makes a field or method private for that class only. Hence, private properties are only accessible from methods of that class, including the constructor. Similarly, private methods can only be executed from other methods of that class, including the constructor.

In the example above, the name property as well as getAge method of the class is private, hence they won’t be accessible outside the class but they can be accessed from within the class. The getNameParts method can access the name property since it is part of the class and it could also access getAge.

Protected Fields

The protected modifier enforces fewer restrictions compared to the private modifier. A protected field or method isn’t accessible outside the class but it is accessible inside a class that inherits it.

In the above example, dob field is private and getAge field is protected hence they won’t be accessible outside the class Person . However since Student class extends Person, getAge method will be accessible inside it.

Parameter Properties

If you carefully examine the constructor function syntax of the classes we have created so far, the boilerplate to initialize properties seems just unnecessary. First, we have to define the fields of the class with types, then we need to mention the parameters of the constructor function with types and then we can initialize their values. Not very productive.

Luckily, our construction function parameters and property names were identical. In such cases, TypeScript provides a handly little syntax to define constructor function parameters and assign field values in one go.

In the above example, the Person class constructor contains parameters with the access modifiers. The TypeScript compiler recognize this signature and implicitly defines name and age modifiers with given access modifiers and assigns the provided values in the constructor call.

This behavior is only valid for parameters with access modifier declarations including protected and readonly modifiers. In the constructor function body, we do have access to this.name and this.age but their values will be already set before the constructor body is executed (just like have properties with default values).

💡 The readonly property is discussed in the Data Immutability lesson (coming soon).

If you are wondering why age property is visible in the output since it’s private, then let me remind you again of the thing we discussed a few seconds ago. TypeScript does not change how JavaScript behaves, it can only enforce the rules during code compilations, therefore ross.age might be inaccessible in the TypeScript program but it will be always accessible at the runtime.

💡 You can also have optional parameter properties. When a parameter property is optional and if the value of that property is not provided in the constructor call, that property will be defined for the object but its value will be undefined. You can use default parameter value syntax to asisgn a default value in case the property value is not provided in the constructor call.

Getters and Setters

At times, your objects have properties whose values should have a certain format. For example, you might wanna store dob (date of birth) property value as standard JavaScript Date format but when the user accesses it, they should get the mm-dd-yyy format.

Similarly, the user should be able to set mm-dd-yyyy value but internally this should get converted to Date object. This seems tricky but JavaScript provides Object.defineProperty method to customize the behavior of such property using get and set accessor settings.

Classes come with this functionality built-in. If we put get keyword in front of a class method, that method behaves like a class property. Whenever we access it like a property, it gets executed and returns a value. Normally, we return a private property value when this function is accessed.

Similarly, the set keyword lets us define a method that acts as a class property but it only executes with an argument when we try to set a value to it. The value we are assigning becomes the argument value. Typically you will update some private property value when this function is executed.

These methods are collectively called accessors. These accessors work in harmony to retrieve and update the value that could be stored in secrecy.

In the above example, the _dob field of the Person class acts as safe storage to save a person’s date of birth since it’s private. Its type is Date which is a type of interface provided by type definition files in the standard library of the TyepScript installation (discussed in Compilation lesson --- coming soon).

The constructor of Person class receives two arguments. The name argument is the public field while the second argument value is stored in the _dob property after converting it to a valid Date object.

We have a dob property getter method which returns the mm-dd-yyyy format of the _dob. The equivalent setter method of the dob property saves the incoming mm-dd-yyyy value as Date object in the _dob property and returns nothing, hence it doesn’t have a return type annotation.

When we access dob property of ross using ross.dob expression, the dob getter method is executed and the return value of the method execution is returned. Similarly, when we set a value in dob field using ross.dob = value expression, the dob setter method is called which updated _dob.

You can have a getter method for a property without having a corresponding setter method. The TypeScript compiler considers such property readonly and it won’t let you write a value to it since it is missing the setter.

Instance Methods

The this object in JavaScript is context-aware. Which means it doesn’t matter how your code looks, this object in the function or method depends on how you are calling the function. Let’s take a look at an example.

In the above example, we have a simple straightforward Person class that has the name public property and the getName method that returns this value. The this object inside getName method points to the instance it is invoked on therefore the ross.getName() call returns the name property value of the ross object. JavaScript as usual, nothing special here.

Later down the code, we are storing ross.getName function into getNameFn variable. Since ross.getName value is a type of function, only the reference of this function is stored in getNameFn and not a copy. Hence when we execute getNameFn(), we are technically executing ross.getName().

However, when we call getNameFn(), it returns undefined. Now for a naked eye, this.name returning undefined doesn’t make any sense because this value inside getName method should be the ross object and ross.name clearly isn’t undefined. So what is happening?

The this value inside a function doesn’t depend on its position in the code. It depends on how the function is called. If a function is called on an owner like obj.method() where obj is an owner, then this value inside the function body is obj. You can learn more about this from this article.

💡 In the case of arrow functions, this value is borrowed from the parent scope of the function. Hence it doesn’t matter how the function is called, it depends on the position of the function since this value in the function is the this value in the enclosing lexical scope.

If the function is called without the owner, this value will be the global object. In the browser, the global object is window. In this situation, since we are running the program on Node, the this value for getNameFn() is global. Since we do not have name variable in the global or name property on the global object, it returns undefined.

💡 In strict mode, if a function call doesn’t have an owner, the default this value will be undefined. Hence the above program won’t work in strict mode since undefined.name expression won’t work as undefined is not an object.

To fix this issue, what we can do is to force getName method to have a fixed this value despite how it is called. This can be done using bind method provided by a function. This method accepts a this context and returns a copy of the method with consistent this.

If you are a React developer, then you understand this syntax pretty well. In the above program, inside the constructor function, we are creating a getName property on the instance itself. Now, any instance of the Person will have the getName property.

The value of this property is a function which is a copy of getName method on the prototype with this bound to the instance itself. This can be a lot to digest but you need to just connect the dots.

Now when you call the ross.getName() method, instead of calling the getName method on the prototype, you are executing function stored in the ross.getName property. This works because the ross.getName property function has this context bounded to ross itself.

In the above example, we have used default property expression to create getName property and assign a function that returns this.name. Notice here, we have used an arrow function which is kind of necessary.

This signature is recognized by the TypeScript compiler and it outputs JavaScript code that adds getName property on the instance of Person. Here, the arrow function is used for signature only but in the actual output, the arrow function is dropped and a consistent this object is referenced from inside the function.

Static Properties and Methods

So far, we have seen how to create class properties and methods that are accessible on an instance of that class. When we call a method on an instance, the this object within that method points to the instance itself. Hence you can easily access properties (and other methods) of the instance within these methods.

Sometimes, you need class level properties and methods. These properties and methods are accessible from the class itself and not from its instance.

💡 Anything except primitive data types is an object in JavaScript, such as a function or an array or class. Hence we can add properties and methods to these types as well.

In the above example, we have used static keyword to mark properties and instances of the class static, which means they now belong to the class. A static property can be only be accessed from the class such as Person.type in the above example or within a static method.

Similarly, a static method can only be accessed from the class or from inside another static method using this.describe() function call. The this object inside a static method will point to the class since a static method is called on the class variable such as Person.describe(), hence Person is the owner of the method in this function call.

Implementing Interfaces

Interfaces describe the shape of an object. We have learned a great deal about interfaces in the Interfaces lesson. An instance of a class is also an object. Hence a class can be forced to implement a contract defined by the interface to produce consistent instance shape.

We use implements keyword with the class to implement an interface. An interface defines the public properties and methods that must be present on the instance. Hence a class implementing an interface must have logic to produce the shape defined by the interface.

In the above example, we have defined a PersonInterface that defines the firstName, lastName, dob and name properties. The name property is readonly, which means its value can only be read and not written. The getBirthYear method defined in the interface returns a number value.

When a class implements an interface, TypeScript forces that class to implements all the properties and all the methods defined in the interface. If you won’t do that, the program won’t compile.

An interface’s job is to provide a shape of an object as seen by the public. It can’t enforce a class to implement private or protected fields or event the static fields since they do not appear in the instance.

Abstract Classes

Sometimes, you have a generic class that extends the functionality of other classes. For example, the Person class might provide some common fields and methods to classes like Student, Player and Racer but you wouldn’t want anyone to create an instance of Person using new Person().

This very Person class is called an abstract class. You can inherit an abstract class using extends keyword but you won’t be able to create an instance of it using new keyword. Abstract classes are only meant to be inherited.

In the above example, we have created a class Person with the abstract keyword which makes this class an abstract. If we try to create an instance of this class, the TypeScript compiler will throw an error.

However, the Student class extends the Person class in a normal fashion. Hence properties and methods of Person class will be accessible on the monica object as usual.

Static vs Instance Side of a class

So far we have seen that an instance of a class has the type of that class. For example, in the above example, since monica is an instance of the Student since it was constructed from the Student using new keyword.

But monica can only have public properties and methods defined by the Student class. Where did the constructor function go? What about static properties and methods?

TypeScript treats a class as both value and type. This implicit type declared by TypeScript describes the shape of the instance a class produces. Therefore when a class is used as a type, such as using let value :Class annotation, TypeScript checks if the value has all the public properties of the Class.

💡 Due to this reason, an interface implemented by the class can only check for public properties of the class and it won’t be able to enforce rules on constructor function or any static members of that class.

So in a nutshell, a class type is nothing but an interface that contains public property declarations of the class. When you create an instance of a class or annotate a value with a class type, this type is called instance side type of the class since it only contains information about what its instances can have.

But in the interfaces lesson, we have learned that an interface can also describe a constructor function. An interface that describes a constructor function describes the static side type of a class. This interface can declare the constructor function type and types of static members.

Sadly, you can’t enforce a class to implement the static side. But this is very helpful to create a factory function that accepts a class as an argument and returns an instance of that class. This argument must have the type of static side of the class since it will be using constructor (and/or static members) to generate an instance from it.

In the above example, PersonInterface defines a constructor function that must accept two string arguments. It also describes delimeter property that must be on this constructor function (class) as well as splitName method.

This interface technically describes a value that can be invoked using new and it has the delimeter and splitName property. The Person class qualifies for this since we can invoke it using new keyword with the exact same argument types and we can also access Person.delimeter and Person.splitName which has exactly the same signature defines by the interface. So in nutshell, PersonInterface is a static side type of the Animal class.

The createPerson function accepts ctor argument of the PersonInterface, hence you can pass Person class an argument. Remember, the Person class doesn’t have the type of Person, its instances have. The real type of Person class (the value) describes the static side of the class which is described by the PersonInterface that has the constructor and static members of the class.

As you can see from the function implementation of the createPerson, the ctor value can be invoked using new and we have no trouble accessing the static members. The return type of this function is Person since the instance of Person class has this type which is created using new Person call.

💡 If your class doesn’t have static members or you don’t care about them inside a factory function, then you can simply use the new (firstName: string, lastName: string) => Person as a type for ctor argument. Read the Function type using an interface section from the interfaces lesson for more details.

I have a simple question, if typeof (new Person) is Person, what is typeof Person? As discussed before, the type of a class is the static side of the class. Hence you don’t need to describe a fancy interface just to annotate the type of a value that must be a class. You can use typeof <Class> instead.

So in nutshell, we can say the following.

  • :typeof Person → Returns static side type of the Person class and it contains the constructor of the class and all static members.
  • :typeof (new Person) or just :``Person → Return instance side type of the Person class and it contains all publically visible members on the instance. It doesn’t contain private or protected members even though they will be on the instance at runtime.

The next section will make much more sense to you now. Since a class has an instance side and it can describe what its instance looks like, it can be used as an interface which leads to many uses cases as you will see.

Class as an Interface

So far we have seen how to create a class and how to inherit from another class using the extends keyword. When a class must have a certain schema, then we use interface keyword to implement an interface.

A class in TypeScript also defines an implicit interface as discussed. Which means, TypeScript also creates an interface from the public members of a class (properties and methods). Hence you can use implements keyword with a class to implements the public schema of another class.

💡 The implicit interface defined by a class is the instance side of the class.

In the above example, Person is a normal class and Student implements the Person class. Hence Student must define properties firstName and lastName as well as getFullName method specified by the Person class.

TypeScript at the moment doesn’t let you implement a class with private or protected fields and there is a good reason for it. You can follow this StackOverflow answer to understand this issue.

💡 You can also implement an abstract class and same rules apply.

In the above example, the Student class is not inheriting anything from the Person class. However, if you need to inherit some functionality of a class but also use the class as an interface, then we have abstract methods to the rescue.

An abstract class can have methods with abstract annotation. These methods are called abstract methods and they do not have a body. These are just a method signature similar to method signatures in an interface. A class inheriting an abstract class must implement these methods.

In the above example is the same program we used earlier in the abstract class sectopm with some modifications. The getFullName method in the Person class is an abstract method, therefore it doesn’t have a body.

Since Student class extends Person class, it inherits firstName and lastName properties. However, since getFullName method is abstract, Student needs to implement this method on its own.

💡 You can also have abstract properties in an abstract class and a subclass would need to implement those as well.

In the interfaces lesson, we learned that an interface can inherit properties from another interface using extends keyword. Now that we know that a class defines an implicit interface, we can extend an interface with a class.

In the above example, class Person has firstName and lastName public properties and getFullName public method. These together form an implicit interface that Student interface inherits. Student interface also declares a marks property of type number of its own.

Hence any object of type Student must have all these 4 fields. Since ross is a type of Student but it doesn’t contain getFullName property, the TypeScript compiler won’t compile the program.

💡 A class can also implement the Student interface or other interfaces can also inherit from the Student interface.

EcmaScript Private Fields

JavaScript now supports the private field natively. Though this feature hasn’t officially released, Google Chrome and Node have support for it. Similar to how we annotate a property in TypeScript class private, we can use # prefix for properties that are supposed to be private in JavaScript.

EcmaScript property adds new behavior to JavaScript objects and there is no way in existing JavaScript to achieve the same behavior. Hence, you won’t be able to find an accurate polyfill for this. For this reason, the TypeScript compiler will only compile the program with this feature implemented as long as you are targeting ES2015 or higher.

#typescript #classes