Chapter 2: Introduction to TypeScript

As we learned in the previous chapter, where we built our very first Angular application, the code of an Angular project is written in TypeScript. Writing in TypeScript and leveraging its static typing gives us a remarkable advantage over other scripting languages. This chapter is not a thorough overview of the TypeScript language. Instead, we'll just focus on the core elements and study them in detail on our journey through Angular. The good news is that TypeScript is not all that complex, and we will manage to cover most of its relevant parts.

In this chapter, we will cover the following topics:

  • Look at the background and rationale behind TypeScript
  • Discover online resources to practice with while we learn
  • Recap on the concept of typed values and their representation
  • Build our custom types, based on classes and interfaces
  • Emphasize the use of advanced types in an Angular project
  • Learn to organize our application architecture with modules

Let's get started!

The history of TypeScript

Transforming small web applications into thick monolithic clients was not possible due to the limitations of earlier JavaScript versions, such as the ECMAScript 5 specification. In a nutshell, large-scale JavaScript applications suffered from serious maintainability and scalability problems as soon as they grew in size and complexity. This issue became more relevant as new libraries and modules required seamless integration into our applications. The lack of proper mechanisms for interoperability led to cumbersome solutions that never seemed to fit the bill.

As a response to these concerns, ECMAScript 6 (also known as ES6 or ES2015) promised to solve these issues by introducing better module loading functionalities, an improved language architecture for better scope handling, and a wide variety of syntactic sugar to better manage types and objects. The introduction of class-based programming turned into an opportunity to embrace a more OOP approach when building large-scale applications.

Microsoft took on this challenge and spent nearly 2 years building a superset of the language, combining the conventions of ES6 and borrowing some proposals from the next version of the specification, ES7. The idea was to launch something that would help build enterprise applications with a lower error footprint using static type checking, better tooling, and code analysis. After 2 years of development led by Anders Hejlsberg, lead architect of C# and creator of Delphi and Turbo Pascal, TypeScript 0.8 was finally introduced in 2012 and reached version 1.0 2 years later. It was not only running ahead of ES6 – it also implemented the same features and provided a stable environment for building large-scale applications. It introduced, among other features, optional static typing through type annotations, thereby ensuring type checking at compile-time and catching errors early in the development process. Its support for declaration files also allows developers to describe the interface of their modules so that other developers can better integrate them into their code workflow and tooling.

The benefits of TypeScript

As a superset of JavaScript, one of the main advantages of embracing TypeScript in your next project is the low entry barrier. If you know JavaScript, you are pretty much all set, since all the additional features in TypeScript are optional. You can pick and introduce any of them to achieve your goal. Overall, there is a long list of strong arguments for advocating for TypeScript in your next project, and all of them apply to Angular as well.

Here is a short rundown, to name a few:

  • Annotating your code with types ensures a consistent integration of your different code units and improves code readability and comprehension.
  • The built-in type-checker analyzes your code at runtime and helps you prevent errors even before executing your code.
  • The use of types ensures consistency across your application. In combination with the previous two, the overall code error footprint gets minimized in the long run.
  • TypeScript extends classes with long-time demanded features such as class fields, private members, and enumerations.
  • The use of decorators allows you to extend your classes and implementations in unique ways.
  • Creating interfaces ensures a smooth and seamless integration of your libraries in other systems and code bases.
  • TypeScript support across different IDEs is terrific, and you can benefit from features such as highlighting code, real-time type checking, and automatic compilation at no cost.
  • The syntax is familiar to developers coming from other OOP-based backgrounds such as Java, C#, and C++.

Introducing TypeScript resources

Let's have a look at where we can get further support to learn and test drive our new knowledge of TypeScript.

Important Note

In this book we will be using TypeScript 3.9 as it is supported by Angular 10

The TypeScript official site

Our first stop is the official website of the language: https://www.typescriptlang.org.

There, we can find more extensive documentation of the language, along with a playground that gives us access to a quick tutorial to get up to speed with the language in no time. It includes some ready-made code examples that cover some of the most common traits of the language. We encourage you to leverage this tool to test the code examples we'll cover throughout this chapter.

The TypeScript official wiki

The code for TypeScript is fully open sourced at GitHub, and the Microsoft team has put reasonable effort into documenting the different facets of the code in the wiki available on the repository site. We encourage you to take a look at it any time you have a question, or if you want to dive deeper into any of the language features or form aspects of its syntax. The wiki is located at https://github.com/Microsoft/TypeScript/wiki.

Types in TypeScript

Working with TypeScript or any other coding language means working with data, and such data can represent different sorts of content that are called types. Types are used to represent the fact that such data can be a text string, an integer value, or an array of these value types, among others. You may have already met types in JavaScript since we have always been working implicitly with them but in a flexible manner. This also means that any given variable could assume (or return, in the case of functions) any value. Sometimes, this leads to errors and exceptions in our code because of type collisions between what our code returned and what we expected it to return type-wise. We can enforce this flexibility using any type, as we will see later in this chapter. However, statically typing our variables gives our IDE and us a good picture of what kind of data we are supposed to find in each instance of code. It becomes an invaluable way to help debug our applications at compile time before it is too late.

String

One of the most widely used primitive types is string, which populates a variable with a piece of text:

var brand: string = 'Chevrolet';

Check out the type definition next to the variable name, which is separated by a colon. This is how we annotate types in TypeScript. We can use either single or double quotes for the value of a string. Feel free to choose either and stick with it within your team. We can define multiline text strings with support for text interpolation with placeholder variables by using backticks:

var brand: string = 'Chevrolet';

var message: string = `Today it's a happy day! I just bought a new ${brand} car`;

In this case, any variables that we may use inside the multiline text must be surrounded by the curly braces of the placeholder ${}.

Declaring variables

TypeScript, as a superset of JavaScript, supports expressive declaration nouns such as let, which denotes that the scope of the variable is the nearest enclosing block (either a function, for loop, or any enclosing statement). On the other hand, const indicates that the value of the declared variable has the same type or value once set.

The let keyword

Traditionally, developers have been using var to declare objects, variables, and other artifacts, but this is discouraged when you start using ES6 or TypeScript. The reason for this is that ES5 only has a function scope; that is, a variable is unique within the context of a function, like so:

function test() {

    var a;

}

There can be no other a variable in this function. If you do declare one, then you effectively redefine it. However, there are cases in which scoping is not applied, such as in for loops. In Java, you would write the following and ensure that a variable will never leak outside of the for loop:

var i = 3;

for (var i = 0; i < 10; i++) {

}

That is, the i variable outside of the for loop will not affect the i variable inside it; they would have a separate scope. But this is not the case with ES5. Thus, ES6 introduced a new feature to fix this flaw, called the let keyword. Consider the following piece of code:

let i = 3;

for (let i = 0; i < 10; i++) {

}

TypeScript compiles it and generates the following JavaScript code:

var i = 3;

for (var i_1 = 0; i_1 < 10; i_1++) {

}

It essentially renames the variable within the for loop so that a name collision doesn't happen. So, remember, no more var; use the let keyword wherever possible.

The const keyword

The const keyword is a way to indicate that a variable should never change. As a code base grows, changes may happen by mistake, and such a mistake might be costly. The const keyword can prevent these types of mistakes through compile-time support. Consider the following code snippet:

const PI = 3.14;

PI = 3;

When the compiler tries to run it, it displays the following error message:

Cannot assign to 'PI' because it is a constant

Notice that this works only at the top level. You need to be aware of this if you declare objects as constants, like so:

const obj = {

    a: 3

};

obj.a = 4; 

Declaring obj as a constant does not freeze the entire object from being edited, but rather what it points to. So, the preceding code is valid.

In the following example, we're actively changing the reference of obj, not one of its properties. Therefore, it is not allowed, and we get the same compiler error that we got previously:

obj = {};

Important Note

const versus let: Prefer to use the const keyword over let when you are sure that the properties of an object will not change during its lifetime. This prevents the object from accidentally changing at runtime and enforces data immutability, a hot topic in Angular applications.

Number

number is probably the other most widespread primitive data type, along with string and boolean:

const age: number = 7;

const height: number = 5.6;

It defines a floating-point number, as well as hexadecimal, decimal, binary, and octal literals.

Boolean

The boolean type defines a variable that can have a value of either true or false:

const isZeroGreaterThanOne: boolean = false;

The result of the variable represents the fulfillment of a boolean condition.

Array

Handling exceptions that arise from errors such as assigning wrong member types in a list can now be easily avoided with the array type, where it defines a list of items that contain certain types only. The syntax requires the postfix [] in the type annotation, as follows:

const brand: string[] = ['Chevrolet', 'Ford', 'General Motors'];

const ages: number[] = [8, 5, 12, 3, 1];

If we try to add a new item to the ages array with a type other than number, the runtime type checker will complain, making sure our typed members remain consistent and that our code is error-free.

Dynamic typing with any type

Sometimes, it is hard to infer the data type out of the information we have at any given point, especially when we are porting legacy code to TypeScript or integrating loosely typed third-party libraries and modules. TypeScript supplies us with a convenient type for these cases. The any type is compatible with all the other existing types, so we can type any data value with it and assign any value to it later:

let distance: any;

distance = '1000km';

distance = 1000;

const distances: any[] = ['1000km', 1000];

However, this great power comes with great responsibility. If we bypass the convenience of static type checking, we are opening the door to type errors when piping data through our modules. It is up to us to ensure type safety throughout our application.

Important Note

The null and undefined literals require special treatment. In a nutshell, they are typed under the any type, which makes it possible to assign these literals to any other variable, regardless of its original type.

Custom types

In TypeScript, you can come up with your own type if you need to by using the type keyword in the following way:

type Animal = 'Cheetah' | 'Lion';

It is essentially a type with a finite number of allowed values. Let's create a variable from this type:

const animal: Animal = 'Cheetah';

This is perfectly valid as Cheetah is one of the allowed values and works as intended.

The interesting part happens when we give our variable a value it does not expect:

const animal: Animal = 'Turtle';

This results in the following compiler error:

Type '"Turtle"' is not assignable to type 'Animal'.

Enum

The enum type is a set of unique numeric values that we can represent by assigning user-friendly names to each of them. Its use goes beyond assigning an alias to a number. We can use it as a way to list the different variations that a specific type can assume, in a convenient and recognizable way. It is defined using the enum keyword and begins numbering members, starting at 0, unless explicit numeric values are assigned to them:

enum Brands { Chevrolet, Cadillac, Ford, Buick, Chrysler, Dodge };

const myCar: Brands = Brands.Cadillac;

Inspecting the value of myCar returns 1 (which is the index held by Cadillac). As we mentioned already, we can assign custom numeric values in enum:

enum BrandsReduced { Tesla = 1, GMC, Jeep };

const myTruck: BrandsReduced = BrandsReduced.GMC;

Inspecting myTruck yields 2, since the first enumerated value, Tesla, was set to 1 already. We can extend value assignation to all the enum members as long as such values are integers:

enum StackingIndex {

    None = 0,

    Dropdown = 1000,

    Overlay = 2000,

    Modal = 3000

};

const mySelectBoxStacking: StackingIndex = StackingIndex.Dropdown;

One last point worth mentioning is the possibility to look up the enum member mapped to a given numeric value:

enum Brands { Chevrolet, Cadillac, Ford, Buick, Chrysler, Dodge };

const myCarBrandName: string = Brands[1];

It should also be mentioned that from TypeScript 2.4 and onward, it is possible to assign string values to enums. This is something that is preferred in Angular projects because of its extended support in template files.

Void

The void type represents the absence of a type, and its use is constrained to annotating functions that do not return an actual value:

function test(): void {

    const a = 0;

}

Therefore, there is no return type in function either.

Type inference

Typing is optional since TypeScript is smart enough to infer the data types of variables and function return values out of context with a certain level of accuracy. When this is not possible, it will assign the dynamic any type to the loosely-typed data at the cost of reducing type checking to a bare minimum. The following is an example of this:

const brand = 'Chevrolet';

This holds the same effect; that is, it leads to a compilation error if you try to assign a non-compatible data type to it.

Functions, lambdas, and execution flow

Functions are the processing machines we used to analyze input, digest information, and apply the necessary transformations to data that's provided either to transform the state of our application or to return an output that will be used to shape our application's business logic or user interactivity.

Functions in TypeScript are not that different from regular JavaScript, except for the fact that, just like everything else in TypeScript, they can be annotated with static types. Thus, they improve the compiler by providing it with the information it expects in their signature and the data type it aims to return, if any.

Annotating types in our functions

The following example showcases how a regular function is annotated in TypeScript:

function sayHello(name: string): string {

    return 'Hello, ' + name;

}

We can see two main differences from the usual function syntax in regular JavaScript. First, we annotate the parameters declared in the function signature, which makes sense since the compiler will want to check whether the data provided holds the correct type. In addition to this, we also annotate the returning value by adding the string type to the function declaration.

As mentioned in the previous section, the TypeScript compiler is smart enough to infer types when no annotation is provided. In this case, the compiler looks into the arguments provided and return statements to infer a returning type from it.

Functions in TypeScript can also be represented as expressions of anonymous functions, where we bind the function declaration to a variable:

const sayHello = function(name: string): string {

    return 'Hello, ' + name;

}  

However, there is a downside to this syntax. Although typing function expressions this way is allowed, thanks to type inference, the compiler is missing the type definition of the declared variable. We might assume that the inferred type of a variable that points to a function typed as a string is a string. Well, it's not. A variable that points to an anonymous function ought to be annotated with a function type:

const sayHello: (name: string) => string = function(name: string): string {

    return 'Hello, ' + name;

}

The function type informs us of the types expected in the function payload and the type returned by the execution of function, if any. This whole block, which is of the form (arguments: type) => returned type, becomes the type annotation our compiler expects.

Function parameters in TypeScript

Due to the type checking performed by the compiler, function parameters require special attention in TypeScript.

Optional parameters

Parameters are a core part of the type checking that's applied by the TypeScript compiler. TypeScript defines that a parameter is optional by adding the ? symbol as a postfix to the parameter name we want to make optional:

function greetMe(name: string, greeting?: string): string {

    if(!greeting) {

        greeting = 'Hello';

    }

    return greeting + ', ' + name;

}

Thus, we can omit the second parameter in the function call:

greetMe('John');

So, an optional parameter is set unless you explicitly do so. It is more of a construct so that you can get help with deciding what parameters are mandatory and which ones are optional. Let's exemplify this:

function add(mandatory: string, optional?: number) {}

You can invoke this function in the following ways:

add('some string');

add('some string', 3.14);

Both versions are valid. Be aware that optional parameters should be placed last in a function signature. Consider the following function:

function add(optional?: number, mandatory: string) {}

This creates a situation where both parameters would be considered mandatory. Let's say you call your function like so:

add(1);

Here, the compiler would complain that you have not provided a value for the mandatory argument. Remember, optional arguments are great, but place them last.

Default parameters

TypeScript gives us another feature to cope with default parameters, where we can set a default value that the parameter assumes when it's not explicitly passed upon executing the function. The syntax is pretty straightforward, as we can see when we refactor the previous example:

function greetMe(name: string, greeting: string = 'Hello'): string {

    return `${greeting}, ${name}`;

}

Just as with optional parameters, default parameters must be put right after the required parameters in the function signature.

Rest parameters

One of the big advantages of the flexibility of JavaScript when defining functions is its ability to accept an unlimited non-declared array of parameters in the form of the arguments object. In a statically typed context such as TypeScript, this might not be possible, but it is actually using the rest parameter's object. We can define, at the end of the arguments list, an additional parameter prefixed by ellipsis ... and typed as an array:

function greetPeople(greeting: string, ...names: string[]): string {

    return greeting + ', ' + names.join(' and ') + '!';

}

So, rest parameters are your friend when you don't know how many arguments you have.

Function overloading

Method and function overloading is a common pattern in other languages, such as C#. However, implementing this functionality in TypeScript clashes with the fact that JavaScript, which TypeScript is meant to compile to, does not implement any elegant way to integrate it out of the box. So, the only workaround possible requires writing function declarations for each of the overloads and then writing a general-purpose function that wraps the actual implementation, whose list of typed arguments and return types are compatible with all the others:

function hello(names: string): string {}

function hello(names: string[]): string {}

function hello(names: any, greeting?: string): string {

    let namesArray: string[];

    if (Array.isArray(names)) {

        namesArray = names;

    } else {

        namesArray = [names];

    }

    if (!greeting) {

        greeting = 'Hello';

    }

    return greeting + ', ' + namesArray.join(' and ') + '!';

}

In the preceding example, we are creating three different function signatures, and each of them features different type annotations. We could even define different return types if there was a case for that. To do so, we should have annotated the wrapping function with any type.

Arrow functions

ES6 introduced the concept of fat arrow functions (also called lambda functions in other languages such as Python, C#, Java, or C++) as a way to simplify the general function syntax and to provide a bulletproof way to handle the scope of the functions. This is something that is traditionally handled by the infamous scope issues of tackling with the this keyword. The first thing we notice is its minimalistic syntax, where, most of the time, we see arrow functions as single-line, anonymous expressions:

const double = x => x * 2;

The function computes the double of a given number x and returns the result, although we do not see any function or return statements in the expression. If the function signature contains more than one argument, we need to wrap them all between braces:

const add = (x, y) => x + y;

Arrow functions can also contain statements. In this case, we want to wrap the whole implementation in curly braces:

const addAndDouble = (x, y) => {

    const sum = x + y;

    return sum * 2;

}

Still, what does this have to do with scope handling? The value of this can point to a different context, depending on where we execute the function. This is a big deal for a language that prides itself on excellent flexibility for functional programming, where patterns such as callbacks are paramount. When referring to this inside a callback, we lose track of the upper context, which usually leads us to using conventions such as assigning its value to a variable named self or that. It is this variable that is used later on within the callback. Statements containing interval or timeout functions make for a perfect example of this:

function delayedGreeting(name): void {

    this.name = name;

    this.greet = function(){

        setTimeout(function() {

            console.log('Hello ' + this.name);

        }, 0);

    }

}

const greeting = new delayedGreeting('John');

greeting.greet();

Executing the preceding script won't give us the expected result of Hello John, but an incomplete string highlighting a pesky greeting to Mr. Undefined! This construction screws the lexical scoping of this when evaluating the function inside the timeout call. Porting this script to arrow functions will do the trick, though:

function delayedGreeting(name): void {

    this.name = name;

    this.greet = function() {

        setTimeout(() => 

            console.log('Hello ' + this.name)

        , 0);

    }

}

Even if we break down the statement contained in the arrow function into several lines of code wrapped by curly braces, the lexical scoping of this keeps pointing to the proper context outside the setTimeout call, allowing for more elegant and clean syntax.

Common TypeScript features

There are some general features in TypeScript that don't apply specifically to classes, functions, or parameters, but instead make coding more efficient and fun. The idea is that the fewer lines of code you have to write, the better it is. It's not only about fewer lines but also about making things more straightforward. There are a ton of such features in ES6 that TypeScript has also implemented. In the following sections, we'll name a few that you are likely going to use in an Angular project.

Spread parameter

A spread parameter uses the same ellipsis syntax as the rest parameters but in a different way. It's not used as a parameter inside of a function, but rather inside its body. Let's illustrate this with an example:

const newItem = 3;

const oldArray = [1, 2];

const newArray = [...oldArray, newItem];

What we do here is add an item to an existing array without changing the old one. oldArray still contains 1, 2, but newArray contains 1, 2, 3. This general principle is called immutability, which essentially means don't change, but rather create a new state from the old state. It's a principle used in functional programming as a paradigm, but also for performance reasons. You can also use a rest parameter on objects, like this:

const oldPerson = { name : 'John' };

const newPerson = { ...oldPerson, age : 20 };

This is a merge between the two objects. Just like with the example of the list, we don't change the previous variable, oldPerson. The newPerson variable takes the information from oldPerson and adds its new values to it.

Template strings

Template strings are all about making your code clearer. Consider the following:

const url = 'http://path_to_domain' +

    'path_to_resource' +

    '?param=' + parameter +

    '=' + 'param2=' +

    parameter2;

So, what's wrong with this? The answer is readability. It's hard to imagine what the resulting string will look like, but it is also easy for you to edit the previous code by mistake, and suddenly, the result will not be what you want. Most languages use a format function for this, and that is exactly what template strings are. This can be used in the following way:

const url =

`${baseUrl}/${path_to_resource}?param=${parameter}&param2={parameter2}`;

This is a much more condensed expression and much easier to read.

Generics

Generics is an expression for indicating a general code behavior that we can employ, regardless of the type of data. They are often used in collections because they have similar behavior, regardless of the type. They can, however, be used on constructs such as methods. The idea is that generics should indicate if you are about to mix types in a way that isn't allowed:

function method<T>(arg: T): T {

    return arg;

}

method<number>(1);

In the preceding example, the type of T is not evaluated until you use the method. As you can see, its type varies, depending on how you call it. It also ensures that you are passing the correct type of data. Suppose that the preceding method is called in this way:

method<string>(1));

We specify that T should be a string, but we insist on passing it a value of the number type. The compiler clearly states that this is not correct. You can, however, be more specific on what T should be. You can make sure that it is an array type so that any value you pass must adhere to this:

function method<T>(arg: T[]): T[] {

    console.log(arg.length);

    return arg;

}

class CustomPerson extends Array {}

class Person {}

const people: Person[] = [];

const newPerson = new CustomPerson();

method<Person>(people);

method<CustomPerson>(newPerson);

In this case, we decide that T should be the Person or CustomPerson type, and that the parameter needs to be of the array type. If we try to pass an object, the compiler will complain:

const person = new Person();

method<Person>(person);

So, why do we do this? We want to ensure that various array methods are available, such as length, and that we, in a given moment, don't care if we operate on something of the CustomPerson or Person type. You can also decide that T should adhere to an interface, like this:

interface Shape {

    area(): number;

}

class Square implements Shape {

    area() { return 1; }

}

class Circle implements Shape {

    area() { return 2; }

}

function allAreas<T extends Shape>(...args: T[]): number {

    let total = 0;

    args.forEach (x => {

        total += x.area();

    });

    return total;

}

allAreas(new Square(), new Circle());

Generics are quite powerful to use if you have a typical behavior that many different data types can relate to. You most likely won't be writing your custom generics, at least not initially, but it's good to know what is going on.

Classes, interfaces, and inheritance

Now that we have overviewed the most relevant bits and pieces of TypeScript, it's time to see how everything falls into place when building TypeScript classes. These classes are the building blocks of Angular applications.

Although class was a reserved word in JavaScript, the language itself never had an actual implementation for traditional POO-oriented classes as other languages such as Java or C# did. 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 by using composition.

Now, we have an actual class functionality, which is flexible and powerful enough to implement the functionality our applications require. 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 in a class come first, and then a constructor and several methods and property accessors follow. None of them contain the reserved function word, and all the members and methods are annotated with a type, except constructor. The following code snippet illustrates what a class could look 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, it won't be publicly exposed as part of the object API.
  • Constructor: The constructor executes right away when we create an instance of the class. Usually, we want to initialize the class members here, with the data provided in the constructor signature. We can also leverage the constructor signature itself 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 saw when analyzing functions in the previous sections, 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 special kind of member that represents a function and, therefore, may return a typed value. It is a function that becomes part of the object API but can be private as well. In this case, they are used as helper functions 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 not related to any specific object instance.
  • Property accessors: To create property accessors (usually pointing to internal private fields, as in the example provided), we need to prefix 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 creating a class, you need to give it a name, define a constructor, and create one or more backing fields, like so:

class Car {

    make: string;

    model: string;

    

    constructor(make: string, model: string) {

        this.make = make;

        this.model = model;

    }

}

For every field you want to add to the class, you usually need to do the following:

  • Add an entry to the constructor
  • Add an assignment in the constructor
  • Declare the field

This is boring and not very productive. TypeScript eliminates this boilerplate by using accessors on the constructor parameters. You can now type the following:

class Car {

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

}

TypeScript will create the respective public fields and make the assignment automatically. 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 and constructs are created, we need to find ways to ensure consistency and rules compliance in our code. One of the best ways to address the consistency and validation of types is to create interfaces. In a nutshell, an interface is a blueprint of the code that defines a particular field's schema. Any artifacts (classes, function signatures, and so on) that implement these interfaces should comply with this schema. This becomes useful 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.

Let's get down to business! In the following code, we're defining the Vehicle interface. It is not a class, but a contractual schema that any class that implements it must comply with:

interface Vehicle {

    make: string;

}

Any class implementing this interface must contain a member named make, which must be typed as a string:

class Car implements Vehicle {

    make: string;

}

Interfaces are, therefore, beneficial to defining the minimum set of members any artifact must fulfill, becoming an invaluable method for ensuring 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, as well as methods in classes and properties in objects, 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 Exception interface that contains a required message and optional id property members:

interface Exception {

    message: string;

    id?: number;

}

In the following code, we're defining the blueprint 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. This is quite useful when we need to define templated constructor or method signatures:

interface ExceptionHandlerSettings {

    logAllExceptions: boolean;

}

Let's bring them all together:

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 });

    }

}

We define a custom error handler class that manages an internal array of exceptions and exposes a logException method to log new exceptions by saving them into the 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 on steroids; let's exemplify that. In the following code, we're declaring an interface, but we're also creating an instance from an interface:

interface A {

    a

}

const instance = <A> { a: 3 };

instance.a = 5;

This is interesting because there are no classes involved. This means you can create a mocking library very easily. Let's explain a what we mean when talking about a mock library. When you are developing code, you might think in terms of interfaces before you even start thinking in terms of concrete classes. This is because you know what methods need to exist, but you might not have decided exactly how the methods should carry out a task.

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 a contract for the database service, an interface, 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());

So, mocking at this point gives us the ability to defer implementation of DatabaseService until we are done writing OrderProcessor. It also makes the testing experience a lot better. Where 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 = <DatabaseService>{};

This creates an instance of DatabaseService. However, be aware that you are responsible for adding a process method to your instance because it starts as an empty object. This 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 away 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 Stuff {

    srv:AuthService = new AuthService();

    

    execute() {

        if (srv.isAuthenticated()) {}

        else {}

    }

}

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

interface AuthService {

    isAuthenticated(): boolean;

}

class Stuff {

    constructor(private srv:AuthService) {}

    execute() {

        if (this.srv.isAuthenticated()) {}

        else {}

    }

}

To test this class, we would typically need to create a concrete implementation of

AuthService and use that as a parameter in the Stuff instance, like this:

class MockAuthService implements AuthService {

    isAuthenticated() { return true; }

}

const srv = new MockAuthService();

const stuff = new Stuff(srv);

It would, however, become quite tedious to write a mock version of every dependency that you wanted to mock away. 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 a class can be defined by an interface, it can also extend the members and functionality of other classes as if they were its own. 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;

    }

}

Here, we're extending from a parent Car class, which already exposes a make member. We can populate the members already defined by the parent class and even execute their constructor by executing 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 are still able to execute the original parent's class methods as it is still accessible from the super object.

Decorators in TypeScript

Decorators are a very cool functionality, initially proposed by Google in AtScript (a superset of TypeScript that finally got merged into TypeScript back in early 2015). They are a part of the current standard proposition for ECMAScript 7. In a nutshell, decorators are a way to add metadata to class declarations for use by dependency injection or compilation directives. By creating decorators, we are defining special annotations that may have an impact on the way our classes, methods, or functions behave or just simply altering the data we define in fields or parameters. In that sense, they are a powerful way to augment our type's native functionalities without creating subclasses or inheriting from other types. It is, by far, one of the most interesting features of TypeScript. It is extensively used in Angular when designing directives and components or managing dependency injection, as we will learn later in Chapter 4, Enhance Components with Pipes and Directives.

The @ prefix can easily recognize decorators in a name, and they are usually located as standalone statements above the element they decorate.

We can define up to four different types of decorators, depending on what element each type is meant to decorate:

  • Class decorators
  • Property decorators
  • Method decorators
  • Parameter decorators

We'll look as these types of decorators in the following subsections.

Important Note

The Angular framework defines its own decorators, which we are going to use during the development of an application.

Class decorators

Class decorators allow us to augment a class or perform operations over its members. The decorator statement is executed before the class gets instantiated. Creating a class decorator requires defining a plain function, whose signature is a pointer to the constructor belonging to the class we want to decorate, typed as a function (or any other type that inherits from the function). The formal declaration defines a ClassDecorator, as follows:

declare type ClassDecorator = <TFunction extends Function>(Target:TFunction) => TFunction | void;

It's complicated to grasp what this gibberish means, isn't it? Let's put everything in context through a simple example, like this:

function Banana(target: Function): void {

    target.prototype.banana = function(): void {

        console.log('We have bananas!');

    }

}

@Banana

class FruitBasket {

    constructor() {}

}

const basket = new FruitBasket();

basket.banana();

As we can see, we have gained a banana method, which was not originally defined in the FruitBasket class, by properly decorating it with the @Banana decorator. It is worth mentioning, though, that this won't compile. The compiler will complain that FruitBasket does not have a banana method, and rightfully so because TypeScript is typed. So, at this point, we need to tell the compiler that this is valid. So, how do we do that? One way is that, when we create our basket instance, we give it the any type, like so:

const basket: any = new FruitBasket();

Another way of essentially accomplishing the same effect is to type this instead:

const basket = new FruitBasket();

(basket as any).banana();

Here, we are doing a conversion on the fly with the as keyword, and we tell the compiler that this is valid.

Extending a class decorator

Sometimes, we might need to customize the way our decorator operates upon instantiating it. We can design our decorators with custom signatures and then have them returning a function with the same signature we defined when designing class decorators with no parameters. The following piece of code illustrates the same functionality as the previous example, but it allows us to customize the message:

function Banana(message: string) {

    return function(target: Function) {

        target.prototype.banana = function(): void {

            console.log(message);

        }

    }

}

@Banana('Bananas are yellow!')

class FruitBasket {

    constructor() {}

}

As a rule of thumb, decorators that accept parameters require a function whose signature matches the parameters we want to configure and returns another function whose signature matches that of the decorator we want to define.

Property decorators

Property decorators are applied to class fields and are defined by creating a PropertyDecorator function, whose signature takes two parameters:

  • target: The prototype of the class we want to decorate
  • key: The name of the property we want to decorate

Possible use cases for this specific type of decorator consist of logging the values assigned to class fields when instantiating objects of such a class, or when reacting to data changes in such fields. Let's see an actual example that showcases both behaviors:

function Jedi(target: Object, key: string) {

    let propertyValue: string = this[key];

    if (delete this[key]) {

        Object.defineProperty(target, key, {

            get: function() {

                return propertyValue;

            },

            set: function(newValue){

                propertyValue = newValue;

                console.log(`${propertyValue} is a Jedi`);

            }

        });

    }

}

class Character {

    @Jedi

    name: string;

}

const character = new Character();

character.name = 'Luke';

The same logic for parameterized class decorators applies here, although the signature of the returned function is slightly different so that it matches that of the parameterless decorator declaration we saw earlier. The following example depicts how we can log changes on a given class property:

function NameChanger(callbackObject: any): Function {

    return function(target: Object, key: string): void {

        let propertyValue: string = this[key];

        if (delete this[key]) {

            Object.defineProperty(target, key, {

                get: function() {

                    return propertyValue;

                },

                set: function(newValue) {

                    propertyValue = newValue;

                    callbackObject.changeName.call(this,                     propertyValue);

                }

            });

        }

    }

}

class Character {

    @NameChanger ({

        changeName: function(newValue: string): void {

            console.log(`You are now known as ${newValue}`);

        }

    })

    name: string;

}

var character = new Character();

character.name = 'Anakin';

A custom function is triggered upon changing that class property.

Method decorators

This decorator can detect, log, and intervene in terms of how methods are executed. To do so, we need to define a MethodDecorator function whose payload takes the following parameters:

  • target: Represents the decorated method (object).
  • key: The actual name of the decorated method (string).
  • value: This is a property descriptor of the given method. It's a hash object containing, among other things, a property named value with a reference to the method itself.

In the following example, we're creating a decorator that displays how a method is called:

function Log(){

    return function(target, propertyKey: string, descriptor: PropertyDescriptor) {

        const oldMethod = descriptor.value;

        descriptor.value = function newFunc( ...args:any[]){

            let result = oldMethod.apply(this, args);

            console.log(`${propertyKey} is called with ${args.            join(',')} and result ${result}`);

            return result;

        }

    }

}

class Hero {

    @Log()

    attack(...args:[]) { return args.join(); }

}

const hero = new Hero();

hero.attack();

This also illustrates what the arguments were upon calling the method, and what the result of the method's invocation was.

Parameter decorator

Our last decorator covers the ParameterDecorator function, which taps into parameters located in function signatures. This sort of decorator is not intended to alter the parameter information or the function behavior, but to look into the parameter value and perform operations elsewhere, such as logging or replicating data. It accepts the following parameters:

  • target: This is the object prototype where the function, whose parameters are decorated, usually belongs to a class.
  • key: This is the name of the function whose signature contains the decorated parameter.
  • parameterIndex: This is the index in the parameters array where this decorator has been applied.

The following example shows a working example of a parameter decorator:

function Log(target: Function, key: string, parameterIndex: number) {

    const functionLogged = key || target.prototype.constructor.    name;

    console.log(`The parameter in position ${parameterIndex} at     ${functionLogged} has been decorated`);

}

class Greeter {

    greeting: string;

    

    constructor (@Log phrase: string) {

        this.greeting = phrase;

    }

}

You have probably noticed the weird declaration of the functionLogged variable. This is because the value of the target parameter varies, depending on the function whose parameters are decorated. Therefore, it is different if we decorate a constructor parameter or a method parameter. The former returns a reference to the class prototype, while the latter returns a reference to the constructor function. The same applies to the key parameter, which is undefined when decorating the constructor parameters.

Parameter decorators do not modify the value of the parameters decorated or alter the behavior of the methods or constructors where these parameters live. Their purpose is usually to log or prepare the container object for implementing additional layers of abstraction or functionality through higher-level decorators, such as a method or class decorator. Usual case scenarios for this encompass logging component behavior or managing dependency injection.

Advanced types

In the Types in Typescript 3.9 section, we learned about some of the basic types in the TypeScript language, which we usually meet in other high-level languages as well. In this section, we'll take a look at some of the advanced types that will help us in the development of an Angular application.

Partial

We use this type when we want to create an object from an interface but include some of its properties, not all of them:

interface Hero {

    name: string;

    power: number;

}

const hero: Partial<Hero> = {

    name: 'Iron man'

}

In the preceding snippet, we can see that the hero object does not include power in its properties.

Record

Some languages, such as C#, have a reserved type when defining a key-value pair object or dictionary, as it is known. In TypeScript, there is no such thing. If we want to define such a type, we declare it as follows:

interface Hero {

    powers: {

        [key: string]: number

    }

}

However, this syntax is not clear. In a real-world scenario, interfaces have many more properties. Alternatively, we can use the Record type to define the interface:

interface Hero {

    powers: Record<string, number>

}

It defines key as a string, which is the name of the power in this case, and the value, which is the actual power factor, as a number.

Union

We've alrady learned about generics and how they help us when we want to mix types. A nice alternative, when we know what the possible types are, is the Union type:

interface Hero {

    name: string;

    powers: number[] | Record<string, number>;

}

In the preceding snippet, we defined the powers property as an array of numbers or a key-value pair collection, nothing more.

Nullable

We mentioned earlier, in the Types in TypeScript 3.9 section, that TypeScript contains two particular basic types, null and undefined, for assigning a variable to anything. We can leverage these types, along with the Union type, to indicate that a property is nullable:

interface Hero {

    powers: number[] | null | undefined;

}

If we want to use the powers property in an object that's of the Hero type, we need to check for nullable values:

const hero: Hero = {

    powers: [10, 20]

}

if (hero.powers !== null && hero.powers !== undefined) {

    for (let i = 0; i < hero.powers.length; i++) {

    }

}

Imagine what happens if we have many nullable properties. We need to type if-else statements for each one separately, which is a cumbersome process. A new feature recently that was added to TypeScript 3.9 comes to the rescue here, known as optional chaining. Essentially, it allows us to write our code so that TypeScript knows to stop execution automatically when it runs into a nullable value. To use it, we need to place the ? postfix in the nullable property, as follows:

for (let i = 0; i < hero.powers?.length; i++) {

}

Now, the if-else statement to check for nullable values is not needed anymore.

Modules

As our applications scale and grow in size, there will be a time when we need to organize our code better and make it sustainable and more reusable. Modules are responsible for this need, so let's take a look at how they work and how we can implement them in our application.

A module works at a file level, where each file is the module itself, and the module name matches the filename without the .ts extension. Each member marked with the export keyword becomes part of the module's public API:

my-service.ts

export class MyService {

    getData() {}

}

To use this module and its exported class, we need to import it:

import { MyService } from './my-service';

Notice that the./my-service path is relative to the location of the file that imports the module. If the module exports more than one artifact, we place them inside the curly braces one by one, separated with a comma:

export class MyService {

    getData() {}

}

export const PI = 3.14;

import { MyService, PI } from './my-service'; 

In the preceding example, MyService exports the getData method and the PI variable in one go.

Summary

This was a long read, but this introduction to TypeScript was necessary to understand the logic behind many of the most brilliant parts of Angular. It gave us the chance to not only introduce the language syntax, but also explain the rationale behind its success as the syntax of choice for building the Angular framework.

We reviewed its type architecture and how we can create advanced business logic when designing functions with a wide range of alternatives for parameterized signatures, and we even discovered how to bypass issues related to scope by using the powerful new arrow functions. Probably the most relevant part of this chapter encompassed our overview of classes, methods, properties, and accessors and how we can handle inheritance and better application design through interfaces. Modules and decorators were some other significant features we explored in this chapter. As we will see very soon, having sound knowledge of these mechanisms is paramount to understanding how dependency injection works in Angular.

With all this knowledge at our disposal, we can now resume our investigation of Angular and confront the relevant parts of component creation, such as style encapsulation, output formatting, and so on, with confidence.

The next chapter will expose us to the basics of a component, how to pass data between components, and how to communicate with them. These features will allow us to put our newly gained knowledge of TypeScript into practice.

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

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