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 orgetFullName
method while a property is strictly akey
inside anobject
(or instance) which isfirstName
in theross
object (instance). When we say class property, we mean a class field that is a property on the instance such asfirstName
.
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 callssuper()
implicitly with all arguments passed in thenew 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 sincethis
value in the function is thethis
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 beundefined
. Hence the above program won’t work in strict mode sinceundefined.name
expression won’t work asundefined
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 anarray
orclass
. 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 forctor
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 thePerson
class and it contains theconstructor
of the class and all static members.:typeof (new Person)
or just:``Person
→ Return instance side type of thePerson
class and it contains all publically visible members on the instance. It doesn’t containprivate
orprotected
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 theStudent
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.