5. Interfaces and Inheritance

Overview

This chapter introduces you to interfaces and inheritance. You will learn how to use an interface to shape your classes, objects, and functions. You will also gain an appreciation of how interfaces will help you to write better code. By the end of this chapter, you will be able to write better, more maintainable code with well-structured functions, classes, and objects, and also be able to reuse your existing code efficiently.

Introduction

The previous chapter discussed classes and objects. You learned that classes define objects and their functionality. Classes are the blueprint followed while constructing these objects. Now, we will go up one level of abstraction. We are now going to construct interfaces. Interfaces are descriptors and allow you to define the structure of your object. Interfaces allow you to define contracts, which are rules that govern how your data is shaped.

Interfaces are important because they enable your objects to be strongly typed, which gives you the ability to write cleaner code. Defining the shape of your objects may not be much of an issue with smaller applications, but when working with large applications, interfaces will prove their worth as they will make it possible for your application to scale without your code becoming confusing and hard to support.

Inheritance allows new objects to take the properties of existing objects, enabling you to extend your code functionality without having to redefine common properties. Inheritance will give you a better understanding of how you should structure your code to be more efficient and logical in your approach. This chapter will first address interfaces and equip you with the skills you need to use them and will then progress onto the topic of inheritance.

Interfaces

Here we have an example of a simple interface that defines the shape of a user object:

interface UserInterFace {

    email: string,

    token: string,

    resetPassword: ()=> boolean

}

In the preceding code, we have defined an interface that we can implement on any object that should follow rules defined in our interface. The advantage this gives us over other web languages such as vanilla JavaScript is that all objects that implement this interface have to follow the structure defined by the interface. This means that our objects are now strongly typed and have language support such as syntax highlighting, autocompletion, and the throwing of exceptions when implemented incorrectly. If you are a developer working on a large application, this is very important as you have defined the rules and can now be sure that all the objects that implement UserInterFace will have the same properties as those defined in the interface.

Here is an example of an object that implements the UserInterface interface:

const User: UserInterFace = {

    email: '[email protected]',

    token: '12345678',

    resetPassword(): boolean{

        return true

    }

}

As you can see in the preceding example, we are now able to implement an object that adheres to the guidelines defined in the UserInterFace interface. When working with large teams or on complex web applications, it is important to have transparent, well-understood rules for your code.

Interfaces allow for the creation of a common point of reference for your objects, a place where rules are defined on how objects should be constructed. In the following section, we will cover in-depth interfaces in TypeScript.

Interfaces are used when you want to set up rules for how your objects, classes, and functions should be implemented. They are a contract that governs structure but not functionality. Here we have a diagram that shows an interface and its relationship to two classes – User and Admin:

Figure 5.1: Relation between interface and classes

Figure 5.1: Relation between interface and classes

In the diagram, we have a user interface that describes how a class belonging to this interface should be implemented. As you can see, we have a few properties (highlighted code in User Interface) and methods provided in two classes. The interface provides only basic information for the property's name, type, method structures, and return types, if not void. Note that the interface provides no rules related to how the methods work, only how they are structured. The actual functionality of the methods is defined in the class itself. As stated earlier, interfaces in TypeScript give you the rules and you implement them as you see fit. This is evident from the preceding diagram. The AdminUser class has a method not defined in UserInterface; however, this is not an issue because the class is in compliance with all the elements of the interface. There is no rule that says that you cannot add to your class, only that you need to meet the requirements of the interface that your class implements.

Case Study – Writing Your First Interface

Imagine you are working with an application development team building an application for warehouse floor workers. You have the task of building the product creation classes and functions. You have developed a plan for your classes based on the functional requirements of your application. You start by creating a product interface called ProductTemplate. ProductTemplate defines the structure of our product object and base requirements. Note that we could also use a type object in the same way, and it may be preferable since this is a simple object, not a class, which could not be represented by a type. However, for the sake of this example and also to enlighten you to the fact that interfaces can also be used as types when defining a simple object, we have constructed the ProductTemplate interface:

Example_Interface_1.ts

1 //first interface

2 interface ProductTemplate {

3 height: number

4 width: number

5 color: string

6 }

When defining an interface, we start with the interface keyword, followed by the name of our interface, ProductTemplate, as shown in the preceding snippet. We have three properties that our product requires – height, width, and color. Now that we have described what our product data should look like, let's use it:

7 //make product function

8 const productMaker = (product: ProductTemplate) => {

9 return product

10 }

We have built a function, productMaker, that takes a product object as an argument. To ensure that only objects with the properties required by our productMaker function get passed to the function, we use our ProductTemplate interface, as shown in the preceding snippet. Now, all we need to do is define our product object; we will use our interface there as well:

11 // implement interface

12 const myProduct: ProductTemplate = {

13 height: 10,

14 width: 12,

15 color: 'red',

16 }

We have declared a product object, myProduct, with our ProductTemplate interface and added the properties required by our interface. Using the interface in this way ensures that we are fully compliant when creating the product object. Now, if we add a property not defined or remove a property that is defined in our ProductTemplate interface, the IDE and or TypeScript compiler will throw a helpful error message. IDE highlighting will depend on your IDE and the level of support for TypeScript. VS Code should highlight the following error messages for the preceding two scenarios.

The following error message appears when you add a property length that is not defined in the interface:

(property) length: number

Type '{ height: number; width: number; color: string; length: number; }' is not assignable to type 'ProductTemplate'.

  Object literal may only specify known properties, and 'length' does not exist in type 'ProductTemplate'.ts(2322)

The following error message appears when you don't use the color property, which is defined in the interface:

const myProduct: ProductTemplate

Property 'color' is missing in type '{ height: number; width: number; }' but required in type 'ProductTemplate'.ts(2741)

Example_Interface.ts(5, 5): 'color' is declared here.

Now that we have our product object, let's pass it to our productMaker function:

// call the function using console log to show the output

console.log(productMaker(myProduct));

Once you run the file using npx ts-node Example_Interface.ts, you will obtain the following output:

{ height: 10, width: 12, color: 'red' }

This is the ideal scenario. But what would happen if you pass an object that does not comply with the ProductTemplate interface? Consider the following code representing this scenario:

const myBadProduct = {

    height: '20',

    color: 1

}

console.log (productMaker(myBadProduct))

You will receive the following error message when you run the file using tsc [filename].ts:

error TS2345: Argument of type '{ height: string; color: number; }' is not assignable to parameter of type 'ProductTemplate'.

  Property 'width' is missing in type '{ height: string; color: number; }' but required in type 'ProductTemplate'.

VS Code prevents you from making such errors. If you hover over the red-underlined code in the VS Code window, you will see a warning similar to the preceding error message.

Let's go back to our interface example (Example_Interface.ts). Now, we have an interface for our product. Let's do the same for our productMaker function. We want to make sure that whenever a function takes our product as an argument, it is constructed in the right way. Hence, we construct the following interface – productInterfaceFunction:

Example_Interface_2.ts

1 // first interface

2 interface ProductTemplate {

3 height: number

4 width: number

5 color: string

6 }

7 //function interface

8 interface productInterfaceFunction {

9 (product: ProductTemplate): ProductTemplate

10 }

We added the function interface, productInterfaceFunction, just after ProductTemplate. As you can see, the syntax is simple and just defines what arguments the function can take and what it should return. We can now use the function interface in our function declaration, as shown here:

//make product function

const productMaker: productInterfaceFunction = (product: ProductTemplate) => {

    return product }

You should again get the same output as before:

{ height: 10, width: 12, color: 'red' }

We have now used interfaces in two ways: to shape an object and a function. The only issue here is that it's not very efficient to work this way. As good developers, we want to be as efficient as possible and comply with object-oriented standards of coding. To this end, we will now refactor our code to define a class that will encapsulate our product properties and methods:

Example_Interface_3.ts

9 //product class interface

10 interface ProductClassInterface {

11 product: ProductTemplate

12 makeProduct(product: ProductTemplate) :ProductTemplate

13 }

In the preceding snippet, we have built an interface for our class where we have defined a product property and the makeProduct method.

We are also making good use of the interfaces we created previously for our product object and makeProduct. Next, we will use the new interface, ProductClassInterface, to instantiate a new class:

16 //class that implements product class interface

17 class ProductClass implements ProductClassInterface {

18 product: ProductTemplate

19 constructor(product: ProductTemplate){

20 this.product = product

21 }

22 makeProduct():ProductTemplate {

23 return this.product;

24 }

25 }

26

27 //new product object

28 const product: ProductTemplate = {height:100, width:200, color: 'pink'}

In the preceding snippet, we are using the implements keyword to apply the interface rules to our ProductClass. The syntax structure is as follows: class ProductClass followed by the implements keyword, and then the interface you would like to apply to the class: class ProductClass implements ProductClassInterface. As you can see, this code is a bit less verbose and easy to manage. Using an interface to define our product class allows us to be more descriptive as we can not only define our class but the methods and properties associated with it.

ype aliases can also be used in a similar manner, but types are more of a validator than a descriptor, hence it is recommended to use types more to verify objects returned from a function or arguments received by a function.

Interfaces and types can be used together, and they should be. However, how they are used, where they are used, and how they are applied in code is down to you, as they are similar in many respects and even more so in recent updates of the TypeScript language. Let's now make a product object and use our class instance, newProduct:

27 //new product object

28 const product: ProductTemplate = {height:100, width:200, color: 'pink'}

29

30 //call make Product function

31 // instantiate product class with new product object

32 const newProduct = new ProductClass(product)

33 // console our new product instance

34 console.log(newProduct.product)

In the preceding snippet, we build a product object and then pass it to our class's makeProduct function. We then console out the results, which is the same as before, except now our functional code is wrapped in a class.

You will obtain the following output:

{ height: 100, width: 200, color: 'pink' }

Now that we have a basic understanding of how to implement an interface with TypeScript, let's build a more realistic product creation process in the following exercise.

Exercise 5.01: Implementing Interfaces

In this exercise, we will implement an interface on an object, function, and class. Some of the code is verbose and you may not implement it this way in a real-world application. However, this exercise will expose you to the different ways in which you can implement interfaces in your code. We will construct a class that manages product objects and use interfaces to enforce rules related to how our class should be implemented. We will also use interfaces to shape our product object and class methods. In a typical web application, this code would probably be part of a product management interface – an inventory management application, for example. Alternativley, it could also be part of the product creation process, where you have a form that takes user data and processes it:

Note

The code file for this exercise can be found here: https://packt.link/SR8eg. For this chapter, in order to run any TypeScript file, you need to go into the file directory and execute npx ts-node filename.ts.

  1. Create an interface called ProductObjectTemplate:

    interface ProductObjectTemplate {

    height: number

    width: number

    color: string

    }

    When creating an interface or a type object for that matter, you should take into consideration what are the common elements your interface or type will need. This could be based on the application requirements or dependent only on the functionality the application is required to have. ProductObjectTemplate is a simple object and, in most cases, should be a type, but in order to show that interfaces can also be used in this way, we have opted to make it an interface. As you can see, we have just defined some basic properties that we may have for a product – height, width, and color.

  2. Using the interface defined in the preceding step, define a function called ProductClassTemplate:

    interface ProductFunctionTemplate {

    (product: ProductObjectTemplate)

    }

    In the preceding step, we used an interface to define a function and, by doing this, we are providing the rules on what arguments your function can take. This will ensure that any implementation of this function will only take ProductObjectTemplate as an argument.

  3. Build an interface for a class called ProductClassTemplate. Reuse ProductFunctionTemplate and ProductObjectTemplate in your new class:

    interface ProductClassTemplate {

    makeProduct: ProductFunctionTemplate

    allProducts():ProductObjectTemplate[]

    }

    In the preceding step, we are reusing the function and product interfaces defined in Steps 1 and 2 to build our class interface. We can simplify the code in this step because we are reusing interfaces that we created in the first two steps. Step 3 is a good example of how you can build complexity while also making your code less verbose.

  4. Create a Product class and implement our class interface:

    class Product implements ProductClassTemplate {

    products: ProductObjectTemplate []

    constructor() {

    this.products = []

    }

    makeProduct(product: ProductObjectTemplate) {

    this.products.push(product)

    }

    allProducts():ProductObjectTemplate[] {

    return this.products

    }}

    In this preceding step, we created our class implementing the ProductClassTemplate interface. This will ensure that our class adheres to the rules defined in our interface. We are also reusing the ProductTemplate interface to verify that our class method takes the right arguments and returns the correct data. In the previous steps, we did a bit of prep work setting up interfaces, and now we can reuse them in our code base, making the overall code easier to write, well supported, and understandable.

  5. Instantiate our class as follows:

    const productInstance: ProductClassTemplate = new Product()const productInstance: ProductClassTemplate = new Product()

    productInstance.makeProduct({})

    Here again, we are making use of an interface, ProductClassTemplate to ensure the class we implement matches our ruleset.

    If we try to call makeProduct with an empty object, we get a helpful error message we can use to resolve our issue. Feel free to perform a test to make sure that your interfaces are working as they should. Here, we have the correct implementation of our class instance method, makeProduct.

  6. Call the makeProduct method and provide a valid product object as defined in our product interface:

    productInstance.makeProduct(

    {

    color: "red",

    height: 10,

    width: 14

    }

    )

  7. Call the allProducts method and console out the results:

    console.log(productInstance.allProducts())

    The allProducts method returns an array of products. This would be the equivalent of an API call that returns a list of products to your frontend.

  8. Now, console out the results of the allProducts method:

    console.log(productInstance.allProducts())

  9. Run the file by executing npx ts-node Exercise01.ts.

    You will obtain the following output:

    [ { color: 'red', height: 10, width: 14 } ]

    Once you have followed the steps correctly, your output should be an array or product object as shown in the preceding screenshot. Interfaces provide you with the means to define contracts that govern how your code should be implemented, which is the point of a strongly typed language such as TypeScript and its main advantage over JavaScript. By using interfaces as shown in the exercise, we now have code that is less prone to errors and easier to support when working with large applications or on a large team. Interfaces can be invaluable to the development process if they are implemented correctly.

Exercise 5.02: Implementing Interfaces – Creating a Prototype Blogging Application

Imagine that you are a developer working on a social networking site. You are tasked with setting up a blogging system that will allow users to post to the site. The project is intended to scale up globally, so it will be quite large. Hence, your code needs to be well defined with all the necessary contexts. The main theme here is context. You are coding in a manner that will lead to bug-free code that is well supported and understood.

First, we start with the main object – the blog post. In order to build a blogging system, we need to define what a blog post is. Because this is a simple object, we create a type alias, BlogPost. As mentioned previously, we can use an interface to define this object, but types are more suited to simple, non-complex objects. A type is more of a descriptor of a unit of something, for example, a number or a string, while an interface is more like directions on how to interact with something, not what it is:

Note

The code file for this exercise can be found here: https://packt.link/6uFmG.

  1. Define a blog type as shown in the following snippet:

    type BlogPost = {

    post: string,

    timeStamp: number,

    user: string

    }

  2. Create an interface called AddToPost:

    interface AddToPost {

    (post: BlogPost): BlogPost []

    }

    This interface will serve as the main interface for the method we will use to add to our blog list. As we elaborated in the previous exercise, the AddToPost interface defines how we will interact with our main method and also what it will return when called.

  3. Create an interface to define a class, BlogPostClass:

    interface IBlogPost {

    allPost: BlogPost [],

    addToPost: AddToPost

    }

    Here, we define our class interface. We know we need a place to hold our blogs, so we define an allPost global object that is of the BlogPost type array. We also define a method, addToPost, that implements the AddPost interface.

  4. Create a class called blogPostClass that implements the blogPostClass interface:

    class blogPostClass implements IBlogPost{

    allPost: BlogPost [] = []

    addToPost(post: BlogPost): BlogPost[] {

    this.allPost = [

    ...this.allPost,

    post

    ]

    return this.allPost

    }

    }

    In the preceding class, we reuse our type to enforce and validate. The logic of the addToPost method is up to you, the developer. In this step, the code implements the method once it adheres to the interface by taking an argument of the BlogPost type and returns a BlogPost array.

  5. Create an instance of blogPostClass:

    const blog = new blogPostClass();

  6. Build three objects of the BlogPost type:

    let post1: BlogPost = {post: 'Goodbye, 2020', timeStamp: 12345678, user: 'Rayon'}

    let post2: BlogPost = {post: 'Welcome, 2021', timeStamp: 12345678, user: 'Mark'}

    let post3: BlogPost = {post: 'What happened to 1999?', timeStamp: 12345678, user: 'Will'}

    This step simulates a user posting to your blog site. In a real-world application, this will be a web form that creates the object when submitted.

  7. Call the addToPost method three times and pass the post objects you created in Step 6:

    blog.addToPost(post1)

    blog.addToPost(post2)

    blog.addToPost(post3)

    In an actual web application, the call to addToPost would entail making an API call to send the updated data to the backend of your application, but for the purpose of this exercise, we are just updating an array. If, for example, you are using some kind of state management for your frontend, the preceding code could look very similar to the state management handling the backend updates.

  8. Console out the allPost global from the class instance created in Step 5:

    console.log(blog.allPost)

  9. Run the file by executing npx ts-node Exercise02.ts.

    You should see the following output:

    [

    { post: 'Goodbye, 2020', timeStamp: 12345678, user: 'Rayon' },

    { post: 'Welcome, 2021', timeStamp: 12345678, user: 'Mark' },

    { post: 'What happened to 1999?', timeStamp: 12345678, user: 'Will' }

    ]

Exercise 5.03: Creating Interfaces for a Function for Updating a User Database

As part of a web app developer team, you have been tasked with building an interface for a function that will update a user database. In a real-world application, this function might be part of a user registration form that updates a user database via an API call. The requirements are simple: the function should take an argument of the User type, which consists of email and userId properties.

For the sake of this exercise, assume that you are just working out the logic of the function and that the code is just temporary for testing purposes before you implement it in your working application. As such, we will have an array that will represent the database, which will be preloaded with some user objects:

Note

The code file for this exercise can be found here: https://packt.link/XLIz9.

  1. Create a user type with email and userId properties, as shown here:

    type User = {

    email: string,

    userId: number

    }

    Creating a user type allows you to simplify your function interface. Now, you can reuse your User type when defining your interface in the next step.

  2. Build a function interface called SuperAddMe, as shown here:

    interface SuperAddMe {

    (user: User): User[]

    };

    In doing this, we have defined how we will interact with our function. This is a small thing, but now, all functions of this type will have set rules. We will know what it needs and what it will return.

  3. Initialize an array of the User type and populate it with a few users:

    let allUsers: User[] = [

    { email: '[email protected]', userId: 1 },

    { email: '[email protected]', userId: 2 }

    ];

    This array will simulate a database of users that we will add to.

  4. Define a function of the SuperAddMe interface type:

    let adduser: SuperAddMe

    adduser = function (user: User): User[] {

    return [

    ...allUsers,

    user

    ]

    }

    When implementing a function in this way, you must first declare it as being of the interface type, which in this case is the SuperAddMe interface. Next, use the function variable and assign a function to it that adheres to the specification of our interface. This implementation is very similar to a type assignment, but because of the complexity of the function, an interface is used. Also, note that this code could be simplified by doing the declaration and assignment on one line, but in order to show the process and make it more readable, the assignment is implemented in parts.

  5. Display the results of a call to a new function, adduser, and pass a user object of the User type. Console out the results to show that the code is working:

    console.log(

    adduser(

    { email: '[email protected]', userId: allUsers.length }

    )

    )

  6. Run the code using the npx ts-node command. You should see the following output:

    [

    { email: '[email protected]', userId: 1 },

    { email: '[email protected]', userId: 2 },

    { email: '[email protected]', userId: 2 }

    ]

Activity 5.01: Building a User Management Component Using Interfaces

Imagine that you are working on a web application and are tasked with building a user management component. You need to build a class to encapsulate the user management aspects of the application and, because you are a good developer, you will be using interfaces to ensure that your code is easy to reuse and support. For this activity, you can assume that your user interface will have at least three properties: email, token, and loginAt. These properties relate to a user's email ID, the web token, and the time on the system when the user logged in.

Note

The code file for this activity can be found here: https://packt.link/xsOhv.

Here are some steps that will help you to complete this activity:

  1. Create a user object interface with the following properties: email : string, loginAt : number, and token: string. The loginAt and token properties should be optional properties.
  2. Build a class interface with a global property, user, and use the interface created in the preceding step to apply user object rules.

    You need to define a getUser method that returns the user object and then use the interface to ensure that the return object is a user object. Finally, define a login method that takes a user object and password(type string) as arguments. Use the user object interface as the user argument type.

  3. Declare a class called UserClass that implements the class interface from the preceding step. Your login method should assign the local function's user argument to the global user property and return the global user. The getUser method should return the global user.
  4. Create an instance of your class declared in Step 2.
  5. Create a user object instance.
  6. Console out our methods to ensure that they are working as expected.

The expected output is as follows:

{ email: '[email protected]', loginAt: 1614068072515, token: '123456' }

{ email: '[email protected]', loginAt: 1614068072515, token: '123456' }

Note

The solution to this activity can be found via this link.

TypeScript was born out of the need to build less confusing, clearly defined code. Interfaces allow you to build out your code in the most structured way possible. Everything has rules and there is no confusion, unlike with vanilla JavaScript.

To summarize the importance of interfaces, you can say that now you can produce code that is better structured and easier for third parties to use.

Let's say, for example, that you built a user class as you did in the preceding activity, and now you need to move on to a different part of your project. The interfaces you have built will be a great help to the developer taking over the user section of the application, or maybe some other developer wants to build a user class with a similar structure to your user class. By using the interfaces you have defined, they can build a structure that follows all the rules you have put in place. This is also helpful as regards debugging, as now they know how things are expected to function and can find where the issues are by using the interfaces as a guideline.

The next section of this chapter is dedicated to inheritance in TypeScript.

TypeScript Inheritance

We will now dive into inheritance, which is one of the core principles of object-oriented programming. It allows us to stay DRY (don't repeat yourself). Inheritance also allows us to be polymorphic, by abstracting functionality. Inheritance gives you the ability to extend your classes from the original class to a child class, which allows you to retain the functionality from the parent or original class and add or override what you don't need.

Child classes can override methods of their parents and have their own methods and objects. Inheritance only allows you to build on the parent class; how you implement your child class is up to you. However, the rule is that there must be some code you need to reuse from your parent class in your child class or you should create a new class as there would be no need to extend a class you don't plan to use any code from.

Let's say you have a user class created to manage users in your application. You are working on a web application and, in the planning stages, you come to the realization that you need more than one user type, as different users will have different levels of access and be able to perform different actions depending on their roles. This is the perfect case for the use of inheritance. Any time you have common properties and functionality, you can extend and not duplicate your code. In this case, we have several user types, which all have common properties of a user: email, createDate, lastLogin, and token, for example.

Because these properties are common to all users, we can put them all into a user class. The user class will serve as the base class that we can extend to our child classes. Your child classes will now have all the common properties without you having to declare them for each child class. As you can see, this is a much more efficient way to do things; it stops code duplication and allows for the consolidation of functionality.

First, let's go over some ground rules of inheritance in TypeScript:

TypeScript only supports inheritance in two ways: single-level and multi-level. Thus, in TypeScript, a child can inherit from a parent (single-level inheritance) or a child can inherit from another child (multi-level inheritance).

Note

They are other types of inheritance, but since Typescript does not support those patterns, this chapter will not address these types here.

Here, we have a diagram of the two types of inheritance that TypeScript supports – single-level and multi-level:

Figure 5.2: An example of single- and multi-level inheritance

Figure 5.2: An example of single- and multi-level inheritance

Single-level inheritance occurs when a child class inherits directly from a parent class, as shown in the preceding diagram. The Son child class is derived from the Father parent class and has all its attributes. It can also have its own properties and functions that are unique to the child class. One of the goals of inheritance is to build on top of an existing base, therefore, just creating a duplicate of the class would be pointless. Multi-level inheritance works the same as single-level inheritance, except the child class inherits from another child class and not directly from the parent, as shown in the preceding diagram. In other words, single-level is derived directly from the base class, which has no parents, while a multi-level child class inherits from a derived class. As you can see, the Grandfather class is the base class and therefore has no parents. Father is derived from GrandFather, but Son, in this case, is derived from Father, making this example multi-level.

TypeScript makes use of the private and public keywords to allow you to hide code from a child class that is private and control how your class properties are accessed by a child class with getter and setter methods. You can override any method that is exposed by a parent class in the child that includes the constructor method by using the keyword super, which is a direct link to the parent class. super also allows you to access properties and methods of the parent class even if they are overridden in your child class.

To see how inheritance works in code, let's go back to our user example that we covered in the introduction to this section. The users of any given application have some common properties, email, createDate, lastLogin, and token, for example. We will use these common elements to build out a base user class:

Examples_Inheritance_1.ts

1 class UserOne {

2 email: string = "";

3 createDate: number = 0;

4 lastLogin: number = 0;

5 token: string = ""

6

7 setToken(token: string): void {

8 // set user token

9 this.token = token;

10 }

11 resetPassword(password: string):string {

12 // return string of new password

13 return password;

14 }

15 }

Here is some information on the properties used in the base class. This will also help you understand why these properties are present in the base class:

  • email: This property serves as a unique identifier.
  • createDate: This property allows you to know when the user was added to the system.
  • lastLogin: This property lets us know when the user was last active on the system.
  • token: This property will validate user requests to the application's API.
  • setToken: This property allows us to set and reset the token property; for example, the user logs out of the application and the token needs to be set to null.
  • resetPassword: This property allows us to reset the current user's password.

We are also using the this keyword to access our class-level token in our setToken function. We have also provided a number of default values in our base class, such as setting an empty string for email and zero for createDate. This just makes it easier to create instances of the class as we do not need to provide values every time we initialize a class instance.

Now, let's move on to inheritance. We will now create a child class, AdminUser:

16 class AdminUser extends UserOne {

17 // pages admin has access to

18 adminPages: string [] = ["admin", "settings"];

19

20 // method that allows the admin to reset other users

21 resetUserPassword(email: string):string {

22 // return default user password

23 return "password123";

24 }

25 }

In order for us to create a child class, we must use the extends keyword followed by the parent class, as shown in the preceding snippet. The syntax structure is as follows: class keyword followed by the name of the child class, the extends keyword, and finally, the name of the parent class you would like to extend: class AdminUser extends UserOne.

Before we move on to some examples, let's list a few things we cannot do with class inheritance in TypeScript:

  • You cannot use other types of inheritance other than single- and multi-level.
  • If you declare a property or a method private, you cannot access it directly in your derived classes.
  • You cannot override the constructor method of your base class unless you call super in your derived class's constructor.

Now, let's go back to our child class, AdminUser. Note that we have added some properties and methods unique to our child class. Unique to AdminUser are adminPages, which is a list of pages only the admin user has access to, and resetUserPassword, which takes an email address of a user and returns a default password:

Note

You can also reference directly the properties and methods of your parent class by using the this keyword in the child class, since AdminUser is now a combined class.

Now, consider the following snippet:

26 // create a instance of our child class

27 const adminUser: AdminUser = new AdminUser()

28

29 // create a string to hold our props

30 let propString = ''

31

32 // loop through your props and appends prop names to propString

33 for(let u in adminUser) {

34 propString += u + ','

35 }

In the preceding snippet, we create an instance of our child class, AdminUser. We also declare a string, propString, as an empty string. This string will hold a list of your class properties. Using a for loop, we loop over our class instance and append the properties to propString.

Now, console out an instance of our child class to verify that we have successfully inherited from our base class:

36 // console out the results

37 console.log(propString)

You should see the properties and methods of our child and parent classes printed on the console:

email,createDate,lastLogin,token,adminPages,constructor,resetUserPassword,setToken,resetPassword,

The preceding output is the expected result. You now have a list of the combined properties of UserOne and AdminUser, showing that we have successfully extended our UserOne class to AdminUser or, in other words, we have shown that AdminUser inherits from UserOne.

Let's now take inheritance up one level by deriving a new class from the AdminUser class. Call the derived class SuperAdmin, because not all admins are created equal:

Examples_Inheritance_2.ts

class SuperAdmin extends AdminUser {

    superPages: string[] = ["super", "ultimate"]

    createAdminUser(adminUser: AdminUser ): AdminUser {

        return adminUser

    }

}

As you can see from the preceding snippet, we are now extending the AdminUser class to create a SuperAdmin class. This means that we now have multi-level inheritance as our current class is inheriting from a derived class. We have also added a new property, superPages, and a method, createAdmin.

Multi-level inheritance is useful for building complexity while still keeping your code easy to manage.

Next, we are going to overload our resetPassword method in the SuperAdmin child class.

We want to create a new method for resetting passwords in our SuperAdmin class. We require a method that adds a hash to make the user password more secure as this will be the admin super user's password:

26 class SuperAdmin extends AdminUser {

27 superPages: string[] = ["super", "ultimate"]

28 readonly myHash: string

29

30 constructor() {

31 super()

32 this.myHash = '1234567'

33 }

34

35 createAdminUser(adminUser: AdminUser ): AdminUser {

36 return adminUser

37 }

38 resetPassword(password: string): string {

39 // add hash to password

40 return password + this.myHash;

41 }

42 }

The preceding code snippet creates a new method, resetPassword, and adds a new myHash property to our SuperAdmin class. We gave our new method the same name, resetPassword, as the resetPassword method in our grandfather class, UserOne. However, this new method returns a password appended with our hash property.

This is called method overriding because the methods have the same name and signature, meaning they take the same arguments. The method in the grandfather class is overridden and the new method will take precedence with instances of the SuperAdmin class.

This is useful when you need to add some functionality to a method in a child class but don't want to change the signature, as the new method does something similar but not exactly the same. Consumers of your code will be able to use the same method but get different outcomes based on which derived child class they invoke.

In the following snippet, we will console out the results of an instance of the SuperAdmin and AdminUser classes and the resetPassword method:

43 const superAdmin = new SuperAdmin()

44 const newAdmin = new AdminUser()

45 console.log( superAdmin.resetPassword('iampassword'))

46 console.log( newAdmin.resetPassword('iampassword'))

You will obtain the following output:

iampassword1234567

iampassword

As you can see from the output, we are calling the same method and getting a different output. This shows that we were able to successfully override the resetPassword method from our parent class, UserOne.

You can also add some access modifiers to our classes to show how they will affect our child classes:

class UserOne {

    email: string = "";

    createDate: number = 0;

    lastLogin: number = 0;

    private token: string = ""

    setToken(token: string): void {

        // set user token

        this.token = token;

    }

    resetPassword(password: string):string {

        // return string of new password

        return password;

}}

In the preceding snippet, we have added the private access modifier to the token property. Now, we can only access the token property through the setToken method, which is public, and all derived classes have access to the setToken method. This is useful in cases where you want to restrict which methods and properties to grant access to in your child classes. This is also useful in cases where you want to abstract functionality, thereby making interfacing with your code easier for consumers.

We want to make sure that every AdminUser class instance is initialized with an email address. Hence, we decide to add a constructor method to our AdminUser class to create an email address for our admin users whenever an AdminUser class is created.

However, we cannot just create a constructor as this is a child class, which means we already have a parent class with a constructor method and we cannot override a constructor method without invoking our base class's constructor method.

To invoke our base class's constructor method, we use super(), which is a direct reference to our base class's constructor method:

// adminUserTwo

class AdminUserTwo extends UserOne {

    // pages admin has access to

    constructor(email: string) {

        super()

        this.email = email;

      }

      

      adminPages: string [] = ["admin", "settings"];

  

      resetUserPassword():string {

          // return default user password

          return "password123";

      }

As you can see in the preceding snippet, we have a constructor method that takes an email address and sets the global email address. We also call the super method so that we can invoke the constructor method on our parent class.

Now, you can create an instance of our AdminUserTwo class and pass an email address when the instance is created. This is all transparent to the user of our AdminUser class:

const adminUserTwo = new AdminUserTwo('[email protected]');

Now that we have covered inheritance, we will put what we have learned to good use in the upcoming exercise.

Exercise 5.04: Creating a Base Class and Two Extended Child Classes

Imagine that you are part of a development team working on a web application for a supermarket chain. You have the task of building a class to represent a user in the application. Because you are a good developer and are aware that you should not try to create one class for all use cases, you will build a base class with common attributes you think all users in your application should have and then extend that as required with child classes:

Note

The code file for this exercise can be found here: https://packt.link/hMd62.

  1. Create a User class, as shown in the following code snippet:

    class User {

    private userName: string;

    private token: string = ''

    readonly timeStamp: number = new Date().getTime()

    constructor(userName: string, token: string) {

    this.userName = userName

    this.token = token

    }

    logOut():void {

    this.userName = ''

    this.token = ''

    }

    getUser() {

    return {

    userName: this.userName,

    token: this.token,

    createdAt: this.timeStamp

    }

    }

    protected renewToken (newToken: string) {

    this.token = newToken

    }}

    The application requires all its users to have username and token upon creation of the user object, so we add those properties and they will be initialized in our constructor.

    We also set them to private as we do not want child classes to access our properties directly. We also have a timestamp property that we will use to set a creation date for the user object. This is set to readonly as it is created when the class is instanced and we don't want it to be modified.

    Different parts of your application will also need to access the properties of your user object. Therefore, we have added getUser, a method that returns your user properties. The getUser method will also allow derived or child classes to access private properties in an indirect way. The application allows the user to be logged in for a set period of time, after which the user token is expired. In order for a user to keep working in the application, we will need to renew their token, so we have added the renewToken method to allow for the setting of the user token property without giving direct access to properties.

  2. Create a Cashier class derived from the User class:

    class Cashier extends User {

    balance: number = 0

    float: number = 0

    start(balance: number, float: number): void {

    this.balance= balance

    this.float = float

    }

    }

    We now have a new user class, Cashier, derived from User, with some unique traits. A user of the Cashier type would need to function in our application. We do not, however, have access to all the properties of our parent class. You cannot access userName and token directly. You are able to access the renewToken method, but not through an instance of the Cashier class. However, you can call that method while building out the Cashier class as part of your user management for cashiers.

    Why would we want to modify access in the child class as opposed to modifying a parent? This is because of encapsulation and standardization: we want to reduce the complexity of our code when consumed by others.

    For example, you have been working on a library of useful functions. You want your coworkers to be able to use it, but they don't need to know the inner workings of your User class. They just need to be able to access the class using the exposed methods and properties. This allows you to guide the process even if you are not the person extending or implementing the code. A good example would be the Date class in JavaScript. You don't need to know how that works. You can simply instance it and use it as directed.

  3. Create an Inventory class derived from User:

    class Inventory extends User {

    products: string [] = []

    // override constructor method, add new prop

    constructor(userName: string, token: string, products: string[]) {

    // call parent constructor method

    super(userName, token)

    // set new prop

    this.products = products

    }}

    Our new user type, Inventory, needs to be able to initialize products upon the declaration of a new inventory user, as this user will be dealing with products directly and should have some products in their user queue when the user logs in to the application.

    In order to make that possible, we have overridden our parent class constructor method in our child class. Our constructor now takes a new argument, products, which is an array of the string type. This means that we have changed the number of arguments our constructor should take based on what we defined in our parent class. Whenever we override our constructor, we need to call super, which is a reference to our parent class.

    As you can see, this allows us to access the parent constructor method, so we can now initialize userName and token and, in doing so, fulfill our child class's parent requirements. The main thing to take away from this is that all our code changes were made in the child class. Your new code for the Inventory class does not affect the other classes derived from User. You have extended and customized your code to deal with unique cases without having to write new code for this user case, saving you time and keeping your code base simple.

    So far, we have derived two classes from our User class, which is single inheritance, as the child classes we created are directly derived from a base class. The next step involves multi-level inheritance.

  4. Create a new derived class, FloorWorker:

    class FloorWorker extends Inventory {

    floorStock: string [] = []

    CheckOut(id: number) {

    if(this.products.length >=0) {

    this.floorStock.push(

    this.products[id]

    )

    }

    }

    }

    This is multi-level inheritance. This class takes into account floor workers. These are users that deal with stocking shelves in the store, so they need to access products from the inventory. They also need to have a count of the products they have removed to stock the store shelves. They need to have access to the User class' properties as well as access to the Products array from the Inventory class.

    In the following code snippet, we will instantiate our different user classes and console out the results of the work we have done so far.

  5. Instantiate your basic user and console out the results:

    const basicUser = new User('user1', '12345678ttt')

    console.log(basicUser)

    You will obtain the following output:

    User {

    token: '12345678ttt',

    timeStamp: 1614074754797,

    userName: 'user1'

    }

  6. Instantiate the Cashier class user and console out the results:

    const cashUser = new Cashier('user2', '12345678')

    console.log(cashUser)

    cashUser.start(10, 1.5)

    console.log(cashUser)

    You will obtain the following output:

    Cashier {

    token: '12345678',

    timeStamp: 1614074754802,

    userName: 'user2',

    balance: 0,

    float: 0

    }

    Cashier {

    token: '12345678',

    timeStamp: 1614074754802,

    userName: 'user2',

    balance: 10,

    float: 1.5

  7. Instantiate the Inventory class user and console out the results:

    // init inventory

    const iUser = new Inventory('user3', '123456789', [

    'orange', 'mango', 'playStation 2'

    ])

    console.log(iUser)

    You will obtain the following output:

    Inventory {

    token: '123456789',

    timeStamp: 1614074754819,

    userName: 'user3',

    products: [ 'orange', 'mango', 'playStation 2' ]

    }

  8. Instantiate the FloorWorker class user and console out the results:

    // FloorWorker

    const fUser = new FloorWorker('user4', '12345678', [

    'orange', 'mango', 'playStation 2'

    ])

    fUser.CheckOut(0)

    console.log(fUser.products)

    console.log(fUser.floorStock)

    You will obtain the following output:

    [ 'orange', 'mango', 'playStation 2' ]

    [ 'orange' ]

    Note

    For steps 5-8, you can also instantiate and console out all your users belonging to the different classes at once, rather than individually, as shown in the exercise.

In this exercise, you created a base class, child classes, and worked on multi-level and single-level inheritance. You also made use of super and access modifiers.

Exercise 5.05: Creating Bases and Extended Classes Using Multi-level Inheritance

You are a developer working at a cell phone company and you are given the task of building a cell phone simulation application. The company manufactures two types of phone – a smartphone and a standard phone. The testing department wants to be able to showcase a number of functions of their phones and requires the ability to add more features to both phone types as the real devices are updated. After looking at the requirements, you come to the realization that you need the ability to model two types of phone and you also want to make it easy to update your code without doing a lot of refactoring and breaking other code that your phone models may use. You also know that both phones have a lot in common – they both have the basic functionality of communicating through voice and text data.

Note

The code file for this exercise can be found here: https://packt.link/pyqDK.

  1. Create a Phone class that will serve as the base class for our child classes, as shown here:

    class Phone {

    powerButton: boolean;

    mic: boolean;

    speaker: boolean;

    serialNumber: string;

    powerOn: boolean = false;

    restart: boolean = false;

    constructor(

    powerButton: boolean,

    mic: boolean,

    speaker: boolean,

    serialNumber: string,

    ) {

    this.powerButton = powerButton

    this.mic = mic;

    this.speaker = speaker;

    this.serialNumber = serialNumber;

    }

    togglePower(): void {

    this.powerOn ? this.powerOn = false : this.powerOn = true

    }

    reboot(): void {

    this.restart = true

    }

    }

    The Phone class is where we will store all the common elements of a phone. This will allow us to simplify our child classes to only deal with the elements unique to them.

  2. Create a Smart class that extends the base or parent class created in Step 1:

    class Smart extends Phone {

    touchScreen: boolean = true;

    fourG: boolean = true;

    constructor(serial: string) {

    super(true, true, true, serial)

    }

    playVideo(fileName: string): boolean {

    return true

    }

    }

    The Smart child class allows us to isolate all the methods and properties of a Smart Phone class.

  3. Create a Standard class that extends the parent class created in Step 1, as shown here:

    class Dumb extends Phone {

    dialPad: boolean = true;

    threeG: boolean = true;

    constructor(serial: string) {

    super(true, true, true, serial)

    }

    NumberToLetter(number: number): string {

    const letter = ['a', 'b', 'c', 'd']

    return letter[number]

    }

    }

    Steps 2 and 3 deal with the creation of our child class, which allows us to meet our goals of being able to update our code without issues and keep our code clean and well maintained. Because we are planning well at this stage, if we need to add features to our Smart phone, we just need to update one child class. This is also true for the Standard phone class. Also, if we have a method or property that we need in both child classes, we only need to update the Phone parent class. With class inheritance, we work smart, not hard.

  4. Create two instances of our child classes and initialize them:

    const smartPhone = new Smart('12345678')

    const standardPhone = new Standard('67890')

  5. Console out and call the unique methods of our class instances to verify that our child classes are working as defined:

    console.log(smartPhone.playVideo('videoOne'))

    console.log(standardPhone.NumberToLetter(3))

    You will obtain the following output:

    true

    d

    if you revisit the respective class definitions of the Smart and Standard classes, you will be able to confirm that the preceding output is indeed evidence of the fact that the classes have worked as expected.

  6. Display the child class instance to show that we have all the properties and methods of our parent class and child classes:

    console.log(smartPhone)

    console.log(standardPhone)

    You will obtain the following output:

    Smart {

    powerOn: false,

    restart: false,

    powerButton: true,

    mic: true,

    speaker: true,

    serialNumber: '12345678',

    touchScreen: true,

    fourG: true

    }

    Dumb {

    powerOn: false,

    restart: false,

    powerButton: true,

    mic: true,

    speaker: true,

    serialNumber: '67890',

    dialPad: true,

    threeG: true

    }

    For this preceding output, too, revisiting the respective class definitions of the Smart and Dumb classes should be proof enough that inheritance, as applied in this exercise, works correctly.

Now that you have an understanding of how inheritance works in TypeScript, we will test our skills in the form of the following activity.

Activity 5.02: Creating a Prototype Web Application for a Vehicle Showroom Using Inheritance

You are tasked with creating a web application for a vehicle showroom. You have decided to use your new skills in inheritance to build out the classes and child classes that will shape the vehicle objects we will require for our complete application. Note that the showroom has several types of vehicles. However, all these types will have some common properties. For example, all vehicles have wheels and a body. You can use this information to build your base class.

The following steps will help you to complete this activity:

Note

The code file for this exercise can be found here: https://packt.link/6Xp8H.

  1. Create a parent class that will hold all common methods and properties for a base vehicle. Define a constructor method that allows you to initialize the base properties of this class and add a method that returns your properties as an object.
  2. Add an access modifier to properties and class methods you want to control access to if necessary.
  3. Derive two child classes from your parent class that are types of vehicles, for example, Car and Truck.
  4. Override your constructor to add some unique properties to your child classes based on the type of vehicle.
  5. Derive a class from one of the child classes created in Step 3, for example, Suv, which will have some of the properties a truck might have, so it would be logical to extend Truck.
  6. Instantiate your child classes and seed them with data.
  7. Console out our child class instance.
  8. The expected output is as follows:

    Car { name: 'blueBird', wheels: 4, bodyType: 'sedan', rideHeight: 14 }

    Truck { name: 'blueBird', wheels: 4, bodyType: 'sedan', offRoad: true }

    Suv {

    name: 'xtrail',

    wheels: 4,

    bodyType: 'box',

    offRoad: true,

    roofRack: true,

    thirdRow: true

    }

    Note

    The solution to this activity can be found via this link.

Summary

In this chapter, we covered interfaces in TypeScript. You learned how interfaces allow you to build contracts around your objects, classes, and methods. You also learned that interfaces are rules that outline how your code is implemented. This chapter covered how using interfaces makes your code easier to understand and is better supported by you and other developers when working in larger teams.

This chapter also taught you about inheritance, one of the core principles of object-oriented programing. You learned about the types of inheritance TypeScript supports and how you can use inheritance to build complexity in your code without making your code more complex. This chapter elucidated that stacking simple structures to make more complex ones is a good practice as it allows you to reuse code and not reinvent the wheel every time you need to build a class. This also lends itself to better code support as you will write only the code you need and have common parent classes that will remain constant throughout your application, thereby making mistakes and bugs easier to find.

You now have a good understanding of interfaces and inheritance, two building blocks you will make good use of as you move forward in this book and in web development using TypeScript.

The concepts you have covered here will make you a better developer overall as now you have the tools to write well-supported, clean, bug-free code.

In the next chapter, you will cover advanced types and will learn about type aliases, type literals, unions, and intersection types.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset