Book Image

Learning Angular - Fourth Edition

By : Aristeidis Bampakos, Pablo Deeleman
5 (1)
Book Image

Learning Angular - Fourth Edition

5 (1)
By: Aristeidis Bampakos, Pablo Deeleman

Overview of this book

As Angular continues to reign as one of the top JavaScript frameworks, more developers are seeking out the best way to get started with this extraordinarily flexible and secure framework. Learning Angular, now in its fourth edition, will show you how you can use it to achieve cross-platform high performance with the latest web techniques, extensive integration with modern web standards, and integrated development environments (IDEs). The book is especially useful for those new to Angular and will help you to get to grips with the bare bones of the framework to start developing Angular apps. You'll learn how to develop apps by harnessing the power of the Angular command-line interface (CLI), write unit tests, style your apps by following the Material Design guidelines, and finally, deploy them to a hosting provider. Updated for Angular 15, this new edition covers lots of new features and tutorials that address the current frontend web development challenges. You’ll find a new dedicated chapter on observables and RxJS, more on error handling and debugging in Angular, and new real-life examples. By the end of this book, you’ll not only be able to create Angular applications with TypeScript from scratch, but also enhance your coding skills with best practices.
Table of Contents (17 chapters)
15
Other Books You May Enjoy
16
Index

Classes, interfaces, and inheritance

We have already overviewed the most relevant bits and pieces of TypeScript, and now it’s time to see how everything falls into place with TypeScript classes. Classes are a fundamental concept in Angular development because everything in the Angular world is a TypeScript class.

Although the class is a reserved keyword in JavaScript, the language itself never had an actual implementation as in other languages such as Java or C#. JavaScript developers used to mimic this kind of functionality by leveraging the function object as a constructor type and instantiating it with the new operator. Other standard practices, such as extending function objects, were implemented by applying prototypal inheritance or using composition.

The class functionality in TypeScript is flexible and powerful enough to use in our applications. We already had the chance to tap into classes in the previous chapter. We’ll look at them in more detail now.

Anatomy of a class

Property members are declared first in a class, then a constructor, and several other methods and property accessors follow. None contain the reserved function keyword, and all the members and methods are annotated with a type, except the constructor.

The following code snippet illustrates what a class looks like:

class Car {
    private distanceRun: number = 0;
    private color: string;
    
    constructor(private isHybrid: boolean, color: string = 'red') {
        this.color = color;
    }
    
    getGasConsumption(): string {
        return this.isHybrid ? 'Very low' : 'Too high!';
    }
    
    drive(distance: number): void {
        this.distanceRun += distance;
    }
    
    static honk(): string {
        return 'HOOONK!';
    }
    
    get distance(): number {
        return this.distanceRun;
    }
}

The class statement wraps several elements that we can break down:

  • Members: Any instance of the Car class will contain three properties: color typed as a string, distanceRun typed as a number, and isHybrid as a boolean. Class members will only be accessible from within the class itself. If we instantiate this class, distanceRun, or any other member or method marked as private, won’t be publicly exposed as part of the object API.
  • Constructor: The constructor parameter is executed when we create an instance of the class. Usually, we want to initialize the class members inside it with the data provided in the constructor signature. We can also leverage the signature to declare class members, as we did with the isHybrid property.

To do so, we need to prefix the constructor parameter with an access modifier such as private or public. As we learned when analyzing functions, we can define rest, optional, or default parameters, as depicted in the previous example with the color argument, which falls back to red when it is not explicitly defined.

  • Methods: A method is a particular member representing a function and may return a typed value. It is a function that becomes part of the object API but can also be private. In this case, it can be used as a helper function within the internal scope of the class to achieve the functionalities required by other class members.
  • Static members: Members marked as static are associated with the class and not with the object instances of that class. We can consume static members directly without having to instantiate an object first. Static members are not accessible from the object instances, which means they cannot access other class members using the this keyword. These members are usually included in the class definition as helper or factory methods to provide a generic functionality unrelated to any specific object instance.
  • Property accessors: A property accessor is defined by prefixing a typed method with the name of the property we want to expose using the set (to make it writable) and get (to make it readable) keywords.

Constructor parameters with accessors

Typically, when we create a class, we give it a name, define a constructor, and create one or more fields, like so:

class Car {
    make: string;
    model: string;
    
    constructor(make: string, model: string) {
        this.make = make;
        this.model = model;
    }
}

For every field of the class, we usually need to do the following:

  1. Add an entry to the constructor
  2. Assign a value within the constructor
  3. Declare the field

TypeScript eliminates the preceding boilerplate steps by using accessors on the constructor parameters:

class Car {
    constructor(public make: string, public model: string) {}
}

TypeScript will create the respective public fields and make the assignment automatically for us. As you can see, more than half of the code disappears; this is a selling point for TypeScript as it saves you from typing quite a lot of tedious code.

Interfaces

As applications scale and more classes are created, we need to find ways to ensure consistency and rule compliance in our code. One of the best ways to address the consistency and validation of types is to create interfaces. An interface is a code contract that defines a particular schema. Any artifacts such as classes and functions that implement an interface should comply with this schema. Interfaces are beneficial when we want to enforce strict typing on classes generated by factories or when we define function signatures to ensure that a particular typed property is found in the payload.

In the following snippet, we’re defining the Vehicle interface:

interface Vehicle {
    make: string;
}

Any class that implements the preceding interface must contain a member named make, which must be typed as a string:

class Car implements Vehicle {
    make: string;
}

Interfaces are also beneficial for defining the minimum set of members any artifact must fulfill, becoming an invaluable method to ensure consistency throughout our code base.

It is important to note that interfaces are not used just to define minimum class schemas but any type out there. This way, we can harness the power of interfaces by enforcing the existence of specific fields that are used later on as function parameters, function types, types contained in specific arrays, and even variables.

An interface may contain optional members as well. The following is an example of defining an interface that contains a required message and an optional id property member:

interface Exception {
    message: string;
    id?: number;
}

In the following snippet, we define the contract for our future class with a typed array and a method, with its returning type defined as well:

interface ErrorHandler {
    exceptions: Exception[];
    logException(message: string, id?: number): void
}

We can also define interfaces for standalone object types, which is quite useful when we need to define templated constructors or method signatures:

interface ExceptionHandlerSettings {
    logAllExceptions: boolean;
}

Let’s bring them all together by creating a custom error handler class:

class CustomErrorHandler implements ErrorHandler {
    exceptions: Exception[] = [];
    logAllExceptions: boolean;
    
    constructor(settings: ExceptionHandlerSettings) {
        this.logAllExceptions = settings.logAllExceptions;
    }
    
    logException(message: string, id?: number): void {
        this.exceptions.push({message, id });
    }
}

The preceding class manages an internal array of exceptions. It also exposes the logException method to log new exceptions by saving them into an array. These two elements are defined in the ErrorHandler interface and are mandatory.

So far, we have seen interfaces as they are used in other high-level languages, but interfaces in TypeScript are stronger and more flexible; let’s exemplify that. In the following code, we’re declaring an interface, but we’re also telling the TypeScript compiler to treat the instance variable as an A interface:

interface A {
    a: number;
}
const instance = { a: 3 } as A;
instance.a = 5;

An example of demonstrating the preceding code is to create a mocking library. When writing code, we might think about interfaces before we even start thinking about concrete classes because we know what methods need, but we might not have decided what methods will contain.

Imagine that you are building an order module. You have logic in your order module, and you know that, at some point, you will need to talk to a database service. You come up with an interface for the database service, and you defer the implementation of this interface until later. At this point, a mocking library can help you create a mock instance from the interface. Your code, at this point, might look something like this:

interface DatabaseService {
    save(order: Order): void
}
class Order {}
class OrderProcessor {
    
    constructor(private databaseService: DatabaseService) {}
    
    process(order) {
        this.databaseService.save(order);
    }
}
let orderProcessor = new OrderProcessor(mockLibrary.mock<DatabaseService>());
orderProcessor.process(new Order());

Mocking at this point allows us to defer the implementation of DatabaseService until we are done writing the OrderProcessor. It also makes the testing experience a lot better. While in other languages, we need to bring in a mock library as a dependency, in TypeScript, we can utilize a built-in construct by typing the following:

const databaseServiceInstance = {} as DatabaseService;

In the preceding snippet, we create an empty object as a DatabaseService. However, be aware that you are responsible for adding a process method to your instance because it starts as an empty object. It will not raise any problems with the compiler; it is a powerful feature, but it is up to us to verify that what we create is correct. Let’s emphasize how significant this TypeScript feature is by looking at some more cases where it pays off to be able to mock things.

Let’s reiterate that the reason for mocking anything in your code is to make it easier to test. Let’s assume your code looks something like this:

class Auth {
    srv: AuthService = new AuthService();
    
    execute() {
        if (srv.isAuthenticated()) {}
        else {}
    }
}

A better way to test this is to make sure that the Auth class relies on abstractions, which means that the AuthService should be created elsewhere and that we use an interface rather than a concrete implementation. So, we should modify our code so that it looks like this:

interface AuthService {
    isAuthenticated(): boolean;
}
class Auth {
    constructor(private srv: AuthService) {}
    execute() {
        if (this.srv.isAuthenticated()) {}
        else {}
    }
}

To test the preceding class, we would typically need to create a concrete implementation of the AuthService and use that as a parameter in the Auth instance:

class MockAuthService implements AuthService {
    isAuthenticated() { return true; }
}
const srv = new MockAuthService();
const auth = new Auth(srv);

It would, however, become quite tedious to write a mock version of every dependency that you wanted to mock. Therefore, mocking frameworks exist in most languages. The idea is to give the mocking framework an interface from which it would create a concrete object. You would never have to create a mock class, as we did previously, but that would be something that would be up to the mocking framework to do internally.

Class inheritance

Just like an interface can define a class, it can also extend the members and functionality of other classes. We can make a class inherit from another by appending the extends keyword to the class name, including the name of the class we want to inherit its members from:

class Sedan extends Car {
    model: string;
    
    constructor(make: string, model: string) {
        super(make);
        this.model = model;
    }
}

In the preceding class, we extend from a parent Car class, which already exposes a member called make. We can populate the members by the parent class and execute their constructor using the super method, which points to the parent constructor. We can also override methods from the parent class by appending a method with the same name. Nevertheless, we can still execute the original parent’s class methods as it is still accessible from the super object.

Classes and interfaces are basic features of the TypeScript language. As we will see in the following section, decorators enhance the use of classes in an application by extending them with custom functionality.