Use inheritance, abstract classes, and polymorphism in TypeScript
Object oriented programming languages such as C# allow you to inherit
functionality from base class. TypeScript also offers inheritance capabilities
so that your classes can inherit from other classes. TypeScript doesn't support
multiple inheritance (although your class can implement multiple interfaces).
You can also create abstract classes - classes that can't be instantiated on
their own; they must be inherited in other classes. In this article you will
learn all these aspects of TypeScript inheritance.
Let's get started.
I am going to assume that you have worked though the examples discussed in
the previous articles of this article and video series. If that's not the case,
I suggest you read earlier parts of this series to learn the other basics of TypeScript we have been discussing so far.
Add a new TypeScript file called Inheritance.ts and also an HTML file named
Inheritance.html
Suppose you are building a contact management system and you want to create two classes - PersonalContact and
ProfessionalContact in your system. The PersonalContact class is a general
purpose class that represents a personal contact such as a friend or a family
member. The ProfessionalContact represents a business contact such as a
colleague or a financial consultant. These two contacts share a few common
things such as first name and last name. So, we will create a class named
Contact
that wraps this common functionality. Then we inherit the Contact class in two
derived classes namely PersonalContact and ProfessionalContact.
The Contact class is shown below:
class Contact {
protected contactID: number;
protected firstName: string;
protected lastName: string;
constructor(contactID: number,
firstName: string,
lastName: string) {
this.contactID = contactID;
this.firstName = firstName;
this.lastName = lastName;
}
getFullName(): string {
return this.firstName + " " +
this.lastName;
}
}
The Contact class declares three properties - contactID, firstName, and lastName. Notice
that they are marked with protected keyword. Earlier in this series you were
introduced to public and private keywords. The protected access modifier
indicates that the member will be accessible in the class it is declared (Contact)
and the sub-classes (PersonalContact and ProfessionalContact).
The Contact class has a constructor that accepts contactID, firstName, and lastName
values. Inside, the values are stored in the respective properties.
The getFullName() function returns the full name of the person by
concatenating firstName and lastName properties.
The PersonalContact and ProfessipnalContact classes require two enumerations
that contain a list of possible contact types:
enum PersonalContactType {
Friend = "Friend",
FamilyMember = "Family Member",
Acquaintance = "Acquaintance"
}
enum ProfessionalContactType {
Financial = "Financial Services",
Medical = "Medical Services",
Consultant = "General Consultation",
Other = "Other"
}
You are already familiar with string enums from the previous part of this
article series. So, I am not going into details of PersonalContactType and
ProfessionalContactType enums.
The Contact class is inherited by PersonalContact and ProfessionalContact
classes. The PersonalContact class is shown below:
class PersonalContact extends Contact {
private contactType: PersonalContactType;
private email: string;
private phone: string;
private birthDate: Date;
constructor(firstName: string,
lastName: string,
contactType: PersonalContactType,
email:string,
phone:string,
birthDate: Date) {
super(contactID, firstName, lastName);
this.contactID = contactID;
this.contactType = contactType;
this.email = email;
this.phone = phone;
this.birthDate = birthDate;
}
getDetails():string {
let details: string;
details = `#${this.contactID}
${super.getFullName()}
${this.contactType}
${this.email}
${this.phone}
${this.birthDate}`;
return details;
}
getFullName(): string {
return `${this.firstName}
${this.lastName},
${this.birthDate}`;
}
}
The PersonalContact class uses extends keyword to inherit from Contact class.
Since PersonalContact is inheriting from the Contact class, contactID, firstName, and
lastName properties are available to it. Additionally, PersonalContact declares
contactType, email, phone, and birthDate properties.
Then we create the constructor for PersonalContact that accepts various
properties as parameters. Inside, the code calls the base class constructor and
passes contactID, firstName and lastNam to it. This is done using super keyword that
represents the base class of the derived class. Then we set other properties to
their respective values.
The PersonalContact class has a public method called getDetails() that
returns all the property values of the object to the caller as a string. Notice
how the code uses super keyword to invoke the base class getFullName() method.
You can also override a base class method in the derived class. In this
case, PersonalContact redefines getFullName method to include birthDate in the
returned string. Due to this overriding, calling getFullName() on the
PersonalContact object will invoke getFullName() as defined in the
PersonalContact. In the absence of overriding, it would have called the
base class getFullName() method.
The other derived class - ProfessionalContact - is shown below:
class ProfessionalContact extends Contact {
private contactType: ProfessionalContactType;
private company: string;
private emails: string[];
private phones: string[];
private notes: string;
constructor(contactID: number,
firstName: string,
lastName: string,
contactType: ProfessionalContactType,
company: string,
emails: string[],
phones: string[],
notes:string
) {
super(contactID, firstName, lastName);
this.contactType = contactType;
this.company = company;
this.emails = emails;
this.phones = phones;
this.notes = notes;
}
getDetails(): string {
let details: string;
details = `#${this.contactID}
${super.getFullName()}
${this.contactType}
${this.company}`;
return details;
}
getFullName(): string {
return `${this.firstName}
${this.lastName}, ${this.company}`;
}
}
This class is similar to the PersonalContact class but has a different set of
additional properties - emails, phones, company and notes.
Now that you have two derived classes ready let's use them by calling their
getDetails() method.
Add a function DoWork() in the Inheritance.ts file and write the following
code in it:
function DoWork() {
let contact: PersonalContact;
contact = new PersonalContact(1,
"Nancy", "Davolio",
PersonalContactType.Acquaintance,
["nancy@localhost"], ["0123456789"], new Date(1960, 10, 3));
document.getElementById("msg").innerHTML
= contact.getDetails();
}
The DoWork() function declares a variable of PeesonalContact type and creates
an object of PersonalContact. All the property values are passed in the
constructor while instantiating the object. Then getDetails() method of
PersonalContact is called and the result is displayed in msg <div> element.
The DoWork() method is called as soon as the Inheritance.html page is loaded
in the browser:
<body onload="DoWork()">
<div id="msg"></div>
<script src="/TypeScript/Output/Inheritance.js">
</script>
</body>
The following figure shows a sample run of this code:
Now change the DoWork() code as follows:
function DoWork() {
let contact: ProfessionalContact;
contact = new ProfessionalContact(2,
"Andrew", "Fuller",
ProfessionalContactType.Consultant,
"Northwind Traders", "andrew@localhost",
"0123456789","Expert in international marketing");
document.getElementById("msg").innerHTML = contact.getDetails();
}
And the outcome is like this:
TypeScript also supports polymorphism via inheritance. Let's illustrate that
too using a simple fragment of code.
Modify DoWork() as shown below:
function DoWork() {
let contact: Contact;
let result: string;
contact = new PersonalContact(1, "Nancy",
"Davolio", PersonalContactType.Acquaintance,
"nancy@localhost", "0123456789", new Date(1960,10,3));
result = contact.getFullName();
contact = new ProfessionalContact(2,
"Andrew", "Fuller", ProfessionalContactType.Consultant,
"Northwind Traders", "andrew@localhost",
"0123456789", "Expert in international marketing");
result += "<br />" + contact.getFullName();
document.getElementById("msg").innerHTML = result;
}
This time the type of contact variable is base type Contact; it's not PersonalContact or ProfessionalContact.
Firstly, contact is assigned a new object of type PersonalContact. Although
the type of contact variable is Contact you are able to store an object of PersonalContact in it because PersonalContact inherits from
Contact.
Then getFullname() method is called. Recollect that Contact has its own
implementation of getFullName() that returns firstName and lastName to the
caller. And PersonalContact also has its own implementation of getFullName()
that returns firstName, lastName, and birthDate. So, when you call getFullName()
on the contact object which implementation will be invoked? As you might have
guessed, although contact's type is Contact, here getFullName() of PersonalContact
is called because contact holds an object of type PersonalContact.
Then the code assigns an object of ProfessionalContact to the contact
variable. This assignment is valid because ProfessionalContact also inherits
from Contact. The code then invokes the getFullName() method. This time
getFullName() of ProfessionalContact gets executed.
If you output the result in the msg <div>, the following will be displayed:
In our example discussed above, we don't need to instantiate Contact class
directly. We need to instantiate either PersonalContact or ProfessionalContact.
So, in this case we can make the Contact class abstract. An abstract class can
contain properties and methods like any other class but it can't be
instantiated. To use an abstract class you need to inherit from it and the
derived class thus created can be instantiated. An abstract class can also have
abstract methods. An abstract method must be implemented by the derived classes.
The modified Contact class is shown below:
abstract class Contact {
protected contactID: number;
protected firstName: string;
protected lastName: string;
constructor(contactID:number,
firstName: string,
lastName: string) {
this.contactID = contactID;
this.firstName = firstName;
this.lastName = lastName;
}
getFullName(): string {
return this.firstName + " " +
this.lastName;
}
abstract getDetails(): string;
}
As you can see, the Contact class is now marked with abstract keyword.
Properties and methods of the Contact class are quite similar to the previous
case but this time getDetails() abstract method has been added to the class. An
abstract method doesn't have an implementation. The derived classes provide the
respective implementation. In our example PersonalContact and
ProfessionalContact classes contain getDetails() implementation.
You can also watch the companion video
here.
That's it for now! Keep coding!!