3. Functions

Overview

Functions are a basic building block of any application. This chapter teaches you how to unleash the power of TypeScript using versatile functions that have capabilities you may not find in other programming languages. We will talk about the this key and look at function expressions, member functions, and arrow functions. This chapter also discusses function arguments, including rest and default parameters. We will also look at the import and export keywords.

This chapter also teaches you how to write tests that pass different combinations of arguments and compare the expected output with the actual output. We will close the chapter by designing a prototype application and completing it with unit tests.

Introduction

So far, we've learned some of the basics of TypeScript, how to set up a project, and the use of definition files. Now we will delve into the topic of functions, which are going to be the most important tools in your arsenal. Even object-oriented programming paradigms depend heavily on functions as a basic building block of business logic.

Functions, sometimes called routines or methods, are part of every high-level programming language. The ability to reuse segments of code is critical, but functions provide an even more important role than that in that they can be given different arguments, or variables, to act against and produce different results. Writing good functions is the difference between a good program and a great one. You first need to start by learning the syntax before thinking about crafting a good function by considering what arguments it should take and what it should produce.

In this chapter, we will cover three different ways to create functions. We will describe the pitfalls and the proper use of the this keyword. We will look at powerful programming techniques, including currying, functional programming, and the use of closures. We will explore the TypeScript module system and how to share code between modules by means of the import and export keywords. We'll see how functions can be organized into classes and how to refactor JavaScript code into TypeScript. Then we will learn how to use the popular Jest testing framework.

Putting these skills to use, we will design, build, and test a prototype flight booking system.

Functions in TypeScript

A simple definition of function is a set of statements that can be invoked; however, the use and conventions of functions cannot be summarized so easily. Functions in TypeScript have greater utility than in some other languages. In addition to being invoked as normal, functions can also be given as arguments to other functions and can be returned from functions. Functions are actually a special kind of object that can be invoked. This means that in addition to parameters, functions can actually have properties and methods of their own, though this is rarely done.

Only the smallest of programs will ever avoid making heavy use of functions. Most programs will be made up of many .ts files. Those files will typically export functions, classes, or objects. Other parts of the program will interact with the exported code, typically by calling functions. Functions create patterns for reusing your application logic and allow you to write DRY (don't repeat yourself) code.

Before diving into functions, let's perform an exercise to get a glimpse of how functions in general are useful. Don't worry if you do not understand some of the function-related syntax in the exercise. You will be studying all of this later in the chapter. The purpose of the following exercise is only to help you understand the importance of functions.

Exercise 3.01: Getting Started with Functions in TypeScript

To give an example of the usefulness of functions, you will create a program that calculates an average. This exercise will first create a program that does not make use of any functions. Then, the same task of calculating the average will be performed using functions.

Let's get started:

Note

The code file for this exercise can be found at https://packt.link/ZHrsh.

  1. Open VS Code and create a new file called Exericse01.ts. Write the following code that makes no use of functions other than the console.log statement:

    const values = [8, 42, 99, 161];

    let total = 0;

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

        total += values[i];

    }

    const average = total/values.length;

    console.log(average);

  2. Run the file by executing npx ts-node Exercise 01.ts on the terminal. You will get the following output:

    77.5.

  3. Now, rewrite the same code using built-in functions and a function of our own, calcAverage:

    const calcAverage = (values: number[]): number =>     (values.reduce((prev, curr) =>     prev + curr, 0) / values.length);

    const values = [8, 42, 99, 161];

    const average = calcAverage(values);

    console.log(average);

  4. Run the file and observe the output:

    77.5.

    The output is the same, but this code is more concise and more expressive. We have written our own function, but we also make use of the built-in array.reduce function. Understanding how functions work will both enable us to write our own useful functions and make use of powerful built-in functions.

Let's continue to build upon this exercise. Instead of just getting the average, consider a program to calculate a standard deviation. This can be written as procedural code without functions:

Example01_std_dev.ts

1 const values = [8, 42, 99, 161];

2 let total = 0;

3 for (let i = 0; i < values.length; i++) {

4 total += values[i];

5 }

6 const average = total / values.length;

7 const squareDiffs = [];

8 for (let i = 0; i < values.length; i++) {

9 const diff = values[i] - average;

10 squareDiffs.push(diff * diff)

11 }

12 total = 0;

13 for (let i = 0; i < squareDiffs.length; i++) {

14 total += squareDiffs[i];

15 }

16 const standardDeviation = Math.sqrt(total / squareDiffs.length);

17 console.log(standardDeviation);

You will get the following output once you run the file:

58.148516748065035

While we have the correct output, this code is very inefficient as the details of implementation (summing an array in a loop, then dividing by its length) are repeated. Additionally, since functions aren't used, the code would be difficult to debug as individual parts of the program can't be run in isolation. If we have an incorrect result, the entire program must be run repeatedly with minor corrections until we are sure of the correct output. This will not scale to programs that contain thousands or millions of lines of code, as many major web applications do. Now consider the following program:

Example02_std_dev.ts

1 const calcAverage = (values: number[]): number =>

2 (values.reduce((prev, curr) => prev + curr, 0) / values.length);

3 const calcStandardDeviation = (values: number[]): number => {

4 const average = calcAverage(values);

5 const squareDiffs = values.map((value: number): number => {

6 const diff = value - average;

7 return diff * diff;

8 });

9 return Math.sqrt(calcAverage(squareDiffs));

10 }

11 const values = [8, 42, 99, 161];

12 console.log(calcStandardDeviation(values));

The output is as follows:

58.148516748065035

Again, the output is correct and we've reused calcAverage twice in this program, proving the value of writing that function. Even if all the functions and syntax don't make sense yet, most programmers will agree that more concise and expressive code is preferable to large blocks of code that offer no patterns of reuse.

The function Keyword

The simplest way to create a function is with a function statement using the function keyword. The keyword precedes the function name, after which a parameter list is given, and the function body is enclosed with braces. The parameter list for a function is always wrapped in parentheses, even if there are no parameters. The parentheses are always required in TypeScript, unlike some other languages, such as Ruby:

function myFunction() {

  console.log('Hello world!');

}

A function that completes successfully will always return either one or zero values. If nothing is returned, the void identifier can be used to show nothing was returned. A function cannot return more than one value, but many developers get around this limitation by returning an array or object that itself contains multiple values that can be recast into individual variables. Functions can return any of the built-in types in TypeScript or types that we write. Functions can also return complex or inline types (described in later chapters). If the type a function might return can't easily be inferred by the body of the function and a return statement, it is a good idea to add a return type to the function. That looks like this. The return type of void indicates that this function doesn't return anything:

function myFunction(): void {

  console.log('Hello world!');

}

Function Parameters

A parameter is a placeholder for a value that is passed into the function. Any number of parameters can be specified for a function. As we are writing TypeScript, parameters should have their types annotated. Let's change our function so that it requires a parameter and returns something:

Example03.ts

1 function myFunction(name: string): string {

2 return `Hello ${name}!`;

3 }

In contrast to the previous example, this function expects a single parameter identified by name, the type of which has been defined as string(name: string). The function body has changed and now uses a string template to return our greeting message as a template string. We could invoke the function like this:

4 const message = myFunction('world');

5 console.log(message);

You will get the following output once you run the file:

Hello world!

This code invokes myFunction with an argument of 'world' and assigns the result of the function call to a new constant, message. The console object is a built-in object that exposes a log function (sometimes called a method as an object member) that will print the given string to the console. Since myFunction concatenates the given parameter to a template string, Hello world! is printed to the console.

Of course, it isn't necessary to store the function result in a constant before logging it out. We could simply write the following:

console.log(myFunction('world'));

This code will invoke the function and log its result to the console, as shown in the following output:

Hello world!

Many of the examples in this chapter will take this form because this is a very simple way to validate the output of a function. More sophisticated applications use unit tests and more robust logging solutions to validate functions, and so the reader is cautioned against filling applications with console.log statements.

Argument versus Parameter

Many developers use the terms argument and parameter interchangeably; however, the term argument refers to a value passed to a function, while parameter refers to the placeholder in the function. In the case of myFunction('world');, the 'world' string is an argument and not a parameter. The name placeholder with an assigned type in the function declaration is a parameter.

Optional Parameters

One important difference from JavaScript is that TypeScript function parameters are only optional if we postfix them with ?. The function in the previous example, myFunction, expects an argument. Consider the case where we don't specify any arguments:

const message = myFunction();

This code will give us a compilation error: Expected 1 arguments, but got 0. That means the code won't even compile, much less run. Likewise, consider the following snippet, where we provide an argument of the wrong type:

const message = myFunction(5);

Now, the error message reads: Argument of type '5' is not assignable to parameter of type 'string'.

It's interesting that this error message has given the narrowest possible type for the value we tried to pass. Instead of saying argument of type 'number', the compiler sees the type as simply the number 5. This gives us a hint that types can be far narrower than the primitive number type.

TypeScript automatically prevents us from making mistakes such as this by enforcing types. But what if we actually do want to make the parameter optional? One option is, as previously mentioned, to postfix the parameter with ?, as shown in the following code snippet:

Example04.ts

1 function myFunction(name?: string): string {

2 return `Hello ${name}!`;

3 }

Now we can successfully invoke it:

4 const message = myFunction();

5 console.log(message);

Running this command will display the following output:

Hello undefined!

In TypeScript, any variable that has yet to be assigned will have the value of undefined. When the function is executed, the undefined value gets converted to the undefined string at runtime, and so Hello undefined! is printed to the console.

Default Parameters

In the preceding example, the name parameter has been made optional and since it never got a value, we printed out Hello undefined!. A better way to do this would be to give name a default value, as shown here:

Example05.ts

1 function myFunction(name: string = 'world'): string {

2 return `Hello ${name}!`;

3 }

Now, the function will give us the default value if we don't provide one:

4 const message = myFunction();

5 console.log(message);

The output is as follows:

Hello world!

And it will give us the value we passed if we do provide one using the following code:

const message = myFunction('reader');

console.log(message);

This will then display the following output:

Hello reader!

This was pretty straightforward. Now, let's try working with multiple arguments.

Multiple Arguments

Functions can have any number or type of arguments. The argument list is separated by commas. Although your compiler settings can allow you to omit argument types, it is a best practice to enable the noImplicitAny option. This will raise a compiler error if you accidentally omit a type. Additionally, the use of the broad any type is discouraged whenever possible, as was covered in Chapter 1, TypeScript Fundamentals and Overview of Types. Chapter 6, Advanced Types, will give us a deeper dive into advanced types, in particular, intersection and union types, that will help us to ensure that all of our variables have good, descriptive types.

Rest Parameters

The spread operator () may be used as the final parameter to a function. This will take all arguments passed into the function and place them in an array. Let's look at an example of how this works:

Example06.ts

1 function readBook(title: string, ...chapters: number[]): void {

2 console.log(`Starting to read ${title}...`);

3 chapters.forEach(chapter => {

4 console.log(`Reading chapter ${chapter}.`);

5 });

6 console.log('Done reading.');

7 }

Now, the function can be called with a variable argument list:

readBook('The TypeScript Workshop', 1, 2, 3);

The first argument is required. The rest will be optional. We could just decline to specify any chapters to read. However, if we do give additional arguments, they must be of the number type because that's what we've used as the type (number[]) for our rest parameter.

You will obtain the following output once you run the preceding code:

Starting to read The TypeScript Book...

Reading chapter 1.

Reading chapter 2.

Reading chapter 3.

Done reading.

Note that this syntax specifically requires single arguments of the number type. It would be possible to implement the function without a rest parameter and instead expect an array as a single argument:

Example07.ts

1 function readBook(title: string, chapters: number[]): void {

2 console.log(`Starting to read ${title}...`);

3 chapters.forEach(chapter => {

4 console.log(`Reading chapter ${chapter}.`);

5 });

6 console.log('Done reading.');

7 }

The function will now require precisely two arguments:

readBook('The TypeScript Book', [1, 2, 3]);

The output is as follows:

Starting to read The TypeScript Book...

Reading chapter 1.

Reading chapter 2.

Reading chapter 3.

Done reading.

Which is better? That's something you'll need to decide for yourself. In this case, the chapters we want to read are already in array form, and then it probably makes the most sense to pass that array to the function.

Notice that the readBook function includes an arrow function inside it. We'll cover arrow functions in an upcoming section.

Destructuring Return Types

At times, it may be useful for a function to return more than one value. Programmers who have embraced functional programming paradigms often want a function that will return a tuple, or an array of two elements that have different types. Going back to our previous example, if we wanted to calculate both the average and standard deviation for a number array, it might be convenient to have a single function that handles both operations, rather than having to make multiple function calls with the same number array.

A function in TypeScript will only return one value. However, we can simulate returning multiple arguments using destructuring. Destructuring is the practice of assigning parts of an object or array to different variables. This allows us to assign parts of a returning value to variables, giving the impression we are returning multiple values. Let's look at an example:

Example08.ts

1 function paritySort(...numbers: number[]): { evens: number[], odds: 2 number[] } {

3 return {

4 evens: numbers.filter(n => n % 2 === 0),

5 odds: numbers.filter(n => n % 2 === 1)

6 };

7 }

This code uses the filter method of the built-in array object to iterate through each value in an array and test it. If the test returns a true Boolean, the value is pushed into a new array, which is returned. Using the modulus operator to test the remainder will filter our number array into two separate arrays. The function then returns those arrays as properties of an object. We can take advantage of this destructuring. Consider the following code:

const { evens, odds } = paritySort(1, 2, 3, 4);

console.log(evens);

console.log(odds);

Here, we give the function the arguments 1, 2, 3, 4, and it returns the following output:

[2, 4]

[1, 3]

The Function Constructor

Note that the TypeScript language contains an uppercase Function keyword. This is not the same as the lowercase function keyword and should not be used as it is not considered to be secure due to its ability to parse and execute arbitrary code strings. The Function keyword only exists in TypeScript because TypeScript is a superset of JavaScript.

Exercise 3.02: Comparing Number Arrays

TypeScript comparison operators such as === or > only work on primitive types. If we want to compare more complex types, such as arrays, we need to either use a library or implement our own comparison. Let's write a function that can compare a pair of unsorted number arrays and tell us whether the values are equal.

Note

The code file for this exercise can be found at https://packt.link/A0IxN.

  1. Create a new file in VS Code and name it array-equal.ts.
  2. Start with this code, which declares three different arrays and outputs, irrespective of whether or not they are equal:

    const arrayone = [7, 6, 8, 9, 2, 25];

    const arraytwo = [6, 8, 9, 2, 25];

    const arraythree = [6, 8, 9, 2, 25, 7];

    function arrayCompare(a1: number[], a2: number[]): boolean {

      return true;

    }

    console.log(

      `Are ${arrayone} and ${arraytwo} equal?`,

      arrayCompare(arrayone, arraytwo)

    );

    console.log(

      `Are ${arrayone} and ${arraythree} equal?`,

      arrayCompare(arrayone, arraythree)

    );

    console.log(

      `Are ${arraytwo} and ${arraythree} equal?`,

      arrayCompare(arraytwo, arraythree)

    );

    The output will be true for all three comparisons because the function has not been implemented and just returns true.

    Our function, arrayCompare, takes two arrays as arguments and returns a Boolean value to represent whether or not they are equal. Our business rule is that arrays can be unsorted and will be considered equal if all their values match when sorted.

  3. Update arrayCompare with the following code:

    function arrayCompare(a1: number[], a2: number[]): boolean {

      if(a1.length !== a2.length) {

        return false;

      }

      return true;

    }

    In the preceding code, we are testing to see whether the two arrays passed in are equal. The first check we should make is to test whether the arrays have equal length. If they aren't equal in length, then the values can't possibly be equal, so we'll return false from the function. If we hit a return statement during execution, the rest of the function won't be executed.

    At this point, the function will only tell us whether the arrays are equal in length. To complete the challenge, we'll need to compare each value in the arrays. This task will be considerably easier if we sort the values before trying to compare them. Fortunately, the array object prototype includes a sort() method, which will handle this for us. Using built-in functions can save a lot of development hours.

  4. Implement the sort() method to sort array values:

    function arrayCompare(a1: number[], a2: number[]): boolean {

      if(a1.length !== a2.length) {

        return false;

      }

      a1.sort();

      a2.sort();

      return true;

    }

    The sort() method sorts the array elements in place, so it isn't necessary to assign the result to a new variable.

    Finally, we need to loop over one of the arrays to compare each element at the same index. We use a for loop to iterate through the first array and compare the value at each index to the value at the same index in the second array. Since our arrays use primitive values, the !== comparison operator will work.

  5. Use the following for loop to loop over the arrays:

    function arrayCompare(a1: number[], a2: number[]): boolean {

      if(a1.length !== a2.length) {

        return false;

      }

      a1.sort();

      a2.sort();

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

        if (a1[i] !== a2[i]) {

          return false;

        }

      }

      return true;

    }

    Again, we'll return false and exit the function if any of the comparisons fail.

  6. Execute the program using ts-node:

    npx ts-node array-equal.ts

    The program will produce the following output:

    Are 7,6,8,9,2,25 and 6,8,9,2,25,8 equal? false

    Are 2,25,6,7,8,9 and 6,8,9,2,25,7 equal? true

    Are 2,25,6,8,8,9 and 2,25,6,7,8,9 equal? False

  7. Experiment with different array combinations and validate the program is working correctly.

A good function takes an argument list and returns a single value. You now have experience writing a function as well as utilizing built-in functions to solve problems.

Function Expressions

Function expressions differ from function declarations in that they can be assigned to variables, used inline, or invoked immediately – an immediately invoked function expression or IIFE. Function expressions can be named or anonymous. Let's look at a few examples:

Example09.ts

1 const myFunction = function(name: string): string {

2 return `Hello ${name}!`;

3 };

4 console.log(myFunction('function expression'));

You will get the following output:

Hello function expression!

This looks quite a lot like a previous example we looked at, and it works almost exactly the same. Here is the function declaration for comparison:

function myFunction(name: string = 'world'): string {

  return `Hello ${name}!`;

}

The one slight difference is that function declarations are hoisted, meaning they are loaded into memory (along with any declared variables) and, as such, can be used before they are declared in code. It is generally considered bad practice to rely on hoisting and, as such, it is now allowed by many linters. Programs that make heavy use of hoisting can have bugs that are difficult to track down and may even exhibit different behaviors in different systems. One of the reasons why function expressions have become popular is because they don't allow hoisting and therefore avoid these issues.

Function expressions can be used to create anonymous functions, that is, functions that do not have names. This is impossible with function declarations. Anonymous functions are often used as callbacks to native functions. For example, consider the following code snippet with the Array.filter function:

Example10.ts

1 const numbers = [1, 3, 2];

2 const filtered = numbers.filter(function(val) {return val < 3});

3 console.log(filtered);

The output is as follows:

[1, 2]

Remember that in TypeScript (as well as JavaScript), functions are can be given as arguments to, or returned from, other functions. This means that we can give the anonymous function, function(val) { return val < 3 }, as an argument to the Array.filter function. This function is not named and cannot be referred to or invoked by other code. That's fine for most purposes. If we wanted to, we could give it a name:

const filtered = numbers.filter(function myFilterFunc(val) {return val < 3});

There's little point in doing this in most cases, but it might be useful if the function needed to be self-referential, for example, a recursive function.

Note

For more information about callbacks, refer to Chapter 11, Higher-Order Functions and Callbacks in TypeScript.

Immediately invoked function expressions look like this:

Example11.ts

1 (function () {

2 console.log('Immediately invoked!');

3 })();

The function outputs the following:

"Immediately invoked!"

The function is declared inline and then the additional () parentheses at the end invoke the function. The primary use case for an IIFE in TypeScript involves another concept known as closure, which will be discussed later in this chapter. For now, just learn to recognize this syntax where a function is declared and invoked right away.

Arrow Functions

Arrow functions present a more compact syntax and also offer an alternative to the confusing and inconsistent rules surrounding the this keyword. Let's look at the syntax first.

An arrow function removes the function keyword and puts a "fat arrow" or => between the parameter list and the function body. Arrow functions are never named. Let's rewrite the function that logs Hello:

const myFunction = (name: string): string => {

  return `Hello ${name}!`;

};

This function can be made even more compact. If the function simply returns a value, the braces and the return keyword can both be omitted. Our function now looks like this.

const myFunction = (name: string): string => `Hello ${name}!`;

Arrow functions are very frequently used in callback functions. The callback to the preceding filter function can be rewritten using an arrow function. Again, callbacks will be discussed in more detail in Chapter 11, Higher-Order Functions and Callbacks in TypeScript. Here is another example of an arrow function:

Example12.ts

1 const numbers = [1, 3, 2];

2 const filtered = numbers.filter((val) => val < 3);

2 console.log(filtered);

The output is as follows:

[1, 2]

This concise syntax may look confusing at first, so let's break it down. The filter function is a built-in method of the array object in TypeScript. It will return a new array containing all the items in the array that match the criteria in the callback function. So, we are saying for each val, add it to the new array if val is less than 3.

Arrow functions are more than just a different syntax. While function declarations and function expressions create a new execution scope, arrow functions do not. This has implications when it comes to using the this (see below) and new (see Chapter 4, Classes and Objects) keywords.

Type Inference

Let's consider the following code:

const myFunction = (name: string): string => `Hello ${name}!`;

const numbers = [1, 3, 2];

const filtered = numbers.filter((val) => val < 3);

console.log(filtered);

The output is as follows:

[1, 2]

Notice that in the preceding code, we aren't specifying a type for the numbers constant. But wait, isn't this a book on TypeScript? Yes, and now we come to one of the best features of TypeScript: type inference. TypeScript has the ability to assign types to variables when we omit them. When we declare const numbers = [1, 2, 3];, TypeScript will intuitively understand that we are declaring an array of numbers. If we wanted to, we could write const numbers: number[] = [1, 2, 3];, but TypeScript will see these declarations as equal.

The preceding code is 100% valid ES6 JavaScript. This is great because any JavaScript developer will be able to read and understand it, even if they have no experience with TypeScript. However, unlike JavaScript, TypeScript will prevent you from making an error by putting the wrong type of value into the numbers array.

Because TypeScript has inferred the type of our numbers array, we would not be able to add a value other than a number to it; for example, numbers.push('hello'); will result in a compiler error. If we wanted to declare an array that would allow other types, we'd need to declare that explicitly – const numbers: (number | string)[] = [1, 3, 2];. Now, we can later assign a string to this array. Alternatively, an array declared as const numbers = [1, 2, 3, 'abc']; would already be of this type.

Going back to our filter function, this function is also not specifying any type for the parameter or the return type. Why is this allowed? It's our friend, type inference, again. Because we're iterating over an array of numbers, each item in that array must be a number. Therefore, val will always be a number and the type need not be specified. Likewise, the expression val < 3 is a Boolean expression, so the return type will always be a Boolean. Remember that optional means you can always opt to provide a required type and you definitely should if that improves the clarity or readability of your code.

When an arrow function has a single parameter and the type can be inferred, we can make our code slightly more concise by omitting the parentheses around the parameter list. Finally, our filter function may look like this:

Example13.ts

1 const numbers = [1, 3, 2];

2 const filtered = numbers.filter(val => val < 3);

3 console.log(filtered);

The output is as follows:

[1, 2]

The syntax you choose is really a matter of taste, but many experienced programmers gravitate to the more concise syntax, so it's important to at least be able to read and understand it.

Exercise 3.03: Writing Arrow Functions

Now, let's write some arrow functions and get used to that syntax, as well as start to build our utility library. A good candidate for a utility library is a function that might be called. In this exercise, we'll write a function that takes a subject, verb, and list of objects and returns a grammatically correct sentence.

Note

The code file for this exercise can be found at https://packt.link/yIQnz.

  1. Create a new file in VS Code and save it as arrow-cat.ts.
  2. Start with a pattern for the function we need to implement, along with some calls to it:

    export const sentence = (

      subject: string,

      verb: string,

      ...objects: string[]

    ): string => {

      return 'Meow, implement me!';

    };

    console.log(sentence('the cat', 'ate', 'apples', 'cheese', 'pancakes'));

    console.log(sentence('the cat', 'slept', 'all day'));

    console.log(sentence('the cat', 'sneezed'));

    Our sentence function obviously isn't doing what we need it to do. We can modify the implementation to use a template string to output the subject, verb, and objects.

  3. Use the following code to implement a template string to output the subject, verb, and objects:

    export const sentence = (

      subject: string,

      verb: string,

      ...objects: string[]

    ): string => {

      return `${subject} ${verb} ${objects}.`;

    };

    Now, when we execute our program, we get the following output:

    the cat ate apples,cheese,pancakes.

    the cat slept all day.

    the cat sneezed .

    This is readable, but we have a number of issues with capitalization and word spacing. We can add some additional functions to help with these problems. Thinking through what should logically happen for these cases, if there are multiple objects, we'd like commas between them and to use "and" before the final object. If there's a single object, there shouldn't be commas or "and," and if there's no object, there shouldn't be an empty space, as there is here.

  4. Implement a new function to add this logic to our program:

    export const arrayToObjectSegment = (words: string[]): string => {

      if (words.length < 1) {

        return '';

      }

      if (words.length === 1) {

        return ` ${words[0]}`;

      }

      ...

    };

    Here, we implement the easier cases. If there are no objects, we want to return an empty string. If there is just one, we return that object with a leading space. Now, let's tackle the case of multiple objects.

    We will need to add the objects to a comma-separated list, and if we have reached the last object, join it with "and".

  5. To do this, we'll initialize an empty string and loop over the array of objects:

    export const arrayToObjectSegment = (words: string[]): string => {

      if (words.length < 1) {

        return '';

      }

      if (words.length === 1) {

        return ` ${words[0]}`;

      }

      let segment = '';

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

        if (i === words.length - 1) {

          segment += ` and ${words[i]}`;

        } else {

          segment += ` ${words[i]},`;

        }

      }

      return segment;

    };

    By breaking the problem down into small components, we've come up with a function that solves all our use cases. Our return statement from sentence can now be return `${subject} ${verb}${arrayToObjectSegment(objects)}.`;.

    Notice how the function that returns a string can fit right into our string template. Running this, we get the following output:

    the cat ate apples, cheese, and pancakes.

    the cat slept all day.

    the cat sneezed.

    That's almost correct, but the first letter of the sentence should be capitalized.

  6. Use another function to handle capitalization and wrap the whole string template with it:

    export const capitalize = (sentence: string): string => {

      return `${sentence.charAt(0).toUpperCase()}${sentence

        .slice(1)

        .toLowerCase()}`;

    };

  7. This function uses several built-in functions: charAt, toUpperCase, slice, and toLowerCase, all inside a string template. These functions grab the first character from our sentence, make it uppercase, and then concatenate it with the rest of the sentence, all cast to lowercase.

    Now, when we execute the program, we get the desired result:

    The cat ate apples, cheese, and pancakes.

    The cat slept all day.

    The cat sneezed.

To complete this exercise, we wrote three different functions, each serving a single purpose. We could have jammed all the functionality into a single function, but that would make the resulting code less reusable and more complicated to read and test. Building software from simple, single-purpose functions remains one of the best ways to write clean, maintainable code.

Understanding this

Many developers have been frustrated by the this keyword. this nominally points to the runtime of the current function. For example, if a member function of an object is invoked, this will usually refer to that object. The use of this across other contexts may seem inconsistent, and its use can result in a number of unusual bugs. Part of the problem lies in the fact that the keyword is relatively straightforward to use in languages such as C++ or Java and programmers with experience in those languages may expect the TypeScript this to behave similarly.

Let's look at a very simple use case for this:

const person = {

    name: 'Ahmed',

    sayHello: function () {

        return `Hello, ${this.name}!`

    }

}

console.log(person.sayHello());

Here we declare an object that has a property, name, and a method, sayHello. In order for sayHello to read the name property, we use this to refer to the object itself. There's nothing wrong with this code and many programmers will find it quite intuitive.

The problem will come in when we need to declare another function inline, likely as a callback function for something like the filter function we looked at earlier.

Let's imagine we want to encapsulate the arrayFilter function in an object that can have a property to specify the maximum number allowed. This object will have some resemblance to the previous one, and we might expect to be able to employ this to get that maximum value. Let's see what happens when we try:

const arrayFilter = {

    max: 3,

    filter: function (...numbers: number[]) {

        return numbers.filter(function (val) {

            return val <= this.max;

        });

    }

}

console.log(arrayFilter.filter(1, 2, 3, 4));

TypeScript doesn't like my code. I'll have a red squiggly line under this, depending on my editor, and I won't be able to execute my program. Even if the program executes, you will not obtain the intended output.

The problem here is that my use of the function keyword creates a new scope and this no longer has the value I want it to. In fact, it has no value. It is undefined.

The reason for this is that unlike object-oriented languages, such as C++ and Java, the value of this will be determined at runtime and it will be set to the calling scope. In this case, our callback function is not part of any set context or object, and so this is undefined. The fact that it's undefined is really immaterial here. The important part is that it's not what we want.

There have been a number of workarounds to this problem over the years. One of them is that we cache the this reference to another variable and make that variable available in our callback function. Another is that we use the bind member function of the Function prototype to set the this reference. You may come across code that looks like this.

A better solution is to simply use arrow functions instead of function expressions. Not only is the syntax more concise and more modern, but arrow functions do not create a new this context. You get the this reference that you want, that of a top-level object. Let's rewrite the code using an arrow function:

Example14.ts

1 const arrayFilter = {

2 max: 3,

3 filter: function(...numbers: number[]) {

4 return numbers.filter(val => {

5 return val <= this.max;

6 });

7 }

8 }

9 console.log(arrayFilter.filter(1, 2, 3, 4));

The function produces the following output:

[1, 2, 3]

TypeScript no longer complains about this and the code works correctly.

But wait, why are we using a function expression for the filter function and an arrow function for the callback? It's because we actually need the scope-creating capability of function in order for this to have a value. If we rewrote the filter function as an arrow function, this would never be set and we wouldn't be able to access the max property.

This is confusing, to be sure, and it's the reason this is dreaded in TypeScript and JavaScript more than in other languages. The important thing to remember is that when you are programming with this, you want any object or class methods to be function expressions and any callbacks to be arrow functions. That way, you'll always have the correct instance of this.

Chapter 4 , Classes and Objects, will contain a deeper dive into classes and explore other patterns. Let's now use this in an object in the following exercise.

Exercise 3.04: Using this in an Object

For this exercise, we will imagine that we have to implement some accounting software. In this software, each account object will track the total amount due, along with the amount that has been paid, and will have a couple of utility methods to get the current state of the account and the balance that needs to be paid.

Let's start by creating the object with its methods unimplemented. This example will demonstrate a simplified workflow where we print out the account, attempt to pay more than is due (receiving an error), then pay the amount due, and finally the full amount due:

Note

The code file for this exercise can be found at https://packt.link/P6YIf.

  1. Write the following code, which is the basis for starting our program:

    export const account = {

      due: 1000,

      paid: 0,

      status: 'OPEN',

      payAccount: function (amount: number): string {

        return 'unimplemented!';

      },

      printStatus: function (): string {

        return 'unimplemented!';

      },

    };

    console.log(account.printStatus());

    console.log(account.payAccount(1500));

    console.log(account.payAccount(500));

    console.log(account.payAccount(500));

    We need to implement both methods. The printStatus method will just output the total that was due, the amount paid so far, and whether the account is open or closed (or fully paid).

  2. Use a string template to output the status, but in order to access the properties on the account object, use the this keyword:

      printStatus: function (): string {

        return `$${this.paid} has been paid and $${

          this.due - this.paid

        } is outstanding. This account is ${this.status}.`;

      },

    We implement the printStatus function expression as a string template that uses this to access properties on the same object. As a reminder, we must use a function expression here and cannot use an arrow function, even if we might prefer that syntax, because arrow functions do not create a new execution context.

    In case there's any confusion, there's no double dollar sign operator here. The first is a literal indicating the currency, and the second is part of the template string.

    Now let's handle the payment. Our requirements are that if the amount paid exceeds the amount due, we should throw an error and not apply any payment. Otherwise, we track the additional payment. If the balance reaches $0, then we close the account. We should also print the current status following each transaction.

  3. Write the code to handle the payment:

      payAccount: function (amount: number): string {

        if (amount > this.due - this.paid) {

          return `$${amount} is more than the outstanding balance of $${

            this.due - this.paid

          }.`;

        }

        this.paid += amount;

        if (this.paid === this.due) {

          this.status = 'CLOSED';

        }

        return this.printStatus();

      },

  4. Execute the program and check the output:

    $0 has been paid and $1000 is outstanding. This account is OPEN.

    $1500 is more than the outstanding balance of $1000.

    $500 has been paid and $500 is outstanding. This account is OPEN.

    $1000 has been paid and $0 is outstanding. This account is CLOSED

In this exercise, we used function expressions as object methods to access properties on the object. Methods can not only read properties on an object, they can also update them. It's a common pattern in object-oriented programming to have objects that both contain data and have the methods available to access and mutate them. Sometimes, those methods will be set to private and only accessed via accessors such as get and set. More on this subject will be covered in Chapter 4, Classes and Objects.

As we've seen in this exercise, when implementing object-oriented patterns, function expressions are still important to know and understand.

Closures and Scope

In addition to everything else we've discussed so far, functions do something special in TypeScript. When a function is declared (be it a function statement, expression, or arrow function), it encloses any variables in a higher scope. This is called a closure. Any function can be a closure. A closure is simply a function that has enclosed variables.

The concept of scope simply means that each function creates a new scope. As we've seen, functions can be declared inside other functions. The inner function can read any variables declared in the outer function, but the outer function cannot see variables declared in the inner function. This is scope. The following code establishes an outer scope and an inner scope by declaring a second function inline inside an outer function. The inner function is able to access the variables in the outer scope, but the world variable declared in the inner scope is not visible outside that function:

Example15.ts

1 const outer = (): void => {

2 const hello = 'Hello';

3 const inner = (): void => {

4 const world = 'world!';

5 console.log(`${hello} ${world}`);

6 }

7 inner();

8

9 console.log(`${hello} ${world}`);

10 }

11 outer();

The function produces the following output:

Hello world!

ReferenceError: world is not defined

When this function is invoked, the inner log statement is reached and logs "Hello world!", and then the outer log statement is reached and we get ReferenceError. We can fix ReferenceError by adding let world; to the outer function:

Example16.ts

1 const outer = (): void => {

2 const hello = 'Hello';

3 let world;

4 const inner = (): void => {

5 const world = 'world!';

6 console.log(`${hello} ${world}`);

7 }

8 inner();

8

9 console.log(`${hello} ${world}`);

10 }

11 outer();

The function produces the following output:

Hello world!

Hello undefined!

This is because the inner function declared a new world variable that the outer function cannot access. We can drop const from the inner declaration:

Example17.ts

1 const outer = (): void => {

2 const hello = 'Hello';

3 let world;

4 const inner = (): void => {

5 world = 'world!';

6 console.log(`${hello} ${world}`);

7 }

8 inner();

9

10 console.log(`${hello} ${world}`);

11 }

12

13 outer();

The function produces the following output:

Hello world!

Hello world!

The function finally works because the inner function operates against a variable that was declared in the scope of the outer function. It is still visible after the inner scope is exited, so it can be printed out.

Let's look at a more useful example. The Fibonacci sequence is a number set in which the next number is the sum of the two previous numbers: [0, 1, 1, 2, 3, 5, 8, 13, 21, …]. The Fibonacci sequence is often used to help explain recursive functions. In this case, we will instead use it to demonstrate closures by writing a function that will return the next value in the sequence each time it is called.

The logic of our program will be that we will track the current number being returned by our function, the next one that should be, and the amount to increment the number. Each time it is called, all three numbers will be updated to prepare for the next call. One way to do that is to define these values as global scoped variables and write a simple function to update and track them. That might look like this:

Example_Fibbonacci_1.ts

1 let next = 0;

2 let inc = 1;

3 let current = 0;

4

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

6 [current, next, inc] = [next, inc, next + inc];

7 console.log(current);

8 }

The function produces the following output:

0

1

1

2

3

5

8

13

21

34

This program works and returns the desired result, but since it isn't a function, the program will just execute once and stop. If you wanted to get the next Fibonacci number as part of some other process, you wouldn't be able to. If you just wrap it in a function, that won't work either:

Example_Fibbonacci_2.ts

1 const getNext = (): number => {

2 let next = 0;

3 let inc = 1;

4 let current = 0;

5 [current, next, inc] = [next, inc, next + inc];

6 return current;

7 };

8

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

10 console.log(getNext());

11 }

The function produces the following output:

0

0

//...

This function will just return 0 every time it's called because all the variables get re-declared when it's invoked. We can fix that by moving the variables outside the function. That way, they are declared once and modified by the function being invoked.

Our function now sets up the next value to be returned, the amount to increment, and the most recent returned value. On each function call in the loop, it will replace the current value with the next value, the next value with the increment amount, and the increment amount to the sum of the next value plus the previous increment amount. Then it logs out the current value:

Example_Fibbonacci_3.ts

1 let next = 0;

2 let inc = 1;

3 let current = 0;

4

5 const getNext = (): number => {

6 [current, next, inc] = [next, inc, next + inc];

7 return current;

8 };

9

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

11 console.log(getNext());

12 }

The function produces the following output:

0

1

1

2

3

5

8

13

21

34

This works! The reason it works is that the getNext function is able to access the variables in the higher scope. The function is a closure. This will seem standard and expected, but what might be unexpected is that this will work even if the function is exported and called by some other part of the program. This can be illustrated better by creating another function:

Example_Fibbonacci_4.ts

1 const fibonacci = () => {

2 let next = 0;

3 let inc = 1;

4 let current = 0;

5 return () => {

6 [current, next, inc] = [next, inc, next + inc];

7 return current;

8 };

9 };

10 const getNext = fibonacci();

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

12 console.log(getNext());

13 }

The output hasn't changed:

0

1

1

2

3

//...

Calling the fibonacci function will return a new function that has access to the variables declared in fibonacci. If we wanted to run another Fibonacci sequence, we could call fibonacci() again to get a fresh scope with initialized variables:

Example_Fibbonacci_5.ts

1 const fibonacci = () => {

2 let next = 0;

3 let inc = 1;

4 let current = 0;

5 return () => {

6 [current, next, inc] = [next, inc, next + inc];

7 return current;

8 };

9 };

10 const getNext = fibonacci();

11 const getMoreFib = fibonacci();

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

13 console.log(getNext());

14 }

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

16 console.log(getMoreFib());

17 }

We'll see the same output again, but twice this time:

0

1

1

2

//…

21

34

0

1

1

2

//…

Note

For ease of presentation, only a section of the actual output is displayed.

In both cases, the closures have closed over the variables in a higher scope and are still available on function calls. This is a powerful technique, as has been shown, but could potentially lead to memory leaks if not used correctly. Variables declared in a closure like this cannot be garbage-collected while a reference to them still exists.

Exercise 3.05: Creating the Order Factory with Closures

Closures can be tricky to work with, but a common pattern that really brings out the usefulness is sometimes called a factory pattern. This is, simply, a function that returns another function that is all set up and ready for use. In this pattern, a closure is used to make sure that variables can persist between function calls. We'll explore this pattern in this exercise.

Let's start with some code that almost does what we want it to do. We are working on an order system for some sort of garment. Each order that comes in will specify a quantity of the garment in identical color and size. We just have to produce a record of each garment with a unique ID for tracking:

Note

The code file for this exercise can be found at https://packt.link/fsqdd.

  1. Create a new file in VS Code and save it as order.ts. Begin with the following code with some sample calls:

    interface Order {

      id: number;

      color: string;

      size: string;

    }

    export const createOrder = (

      color: string,

      size: string,

      quantity: number

    ): Order[] => {

      let id = 0;

      const orders = [];

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

        orders.push({ id: id++, color, size });

      }

      return orders;

    };

    const orderOne = createOrder('red', 'M', 4);

    console.log(orderOne);

    const orderTwo = createOrder('blue', 'S', 7);

    console.log(orderTwo);

    The code looks OK. Let's run it and see how it works. You will obtain the following output:

    [

      { id: 0, color: 'red', size: 'M' },

      { id: 1, color: 'red', size: 'M' },

      { id: 2, color: 'red', size: 'M' },

      { id: 3, color: 'red', size: 'M' }

    ]

    [

      { id: 0, color: 'blue', size: 'S' },

      { id: 1, color: 'blue', size: 'S' },

      { id: 2, color: 'blue', size: 'S' },

      { id: 3, color: 'blue', size: 'S' },

      { id: 4, color: 'blue', size: 'S' },

      { id: 5, color: 'blue', size: 'S' },

      { id: 6, color: 'blue', size: 'S' }

    ]

    That's not right. We can't start the ID numbers over at zero again each time. How can we fix this problem?

    There are a couple of ways to fix this. The easiest way to do it would be to declare the ID number outside of orderFactory. However, doing that might lead to bugs as system complexity grows. Variables that are in a topmost or even global scope are accessible to every part of the system and may get modified by some edge case.

  2. Use a closure to solve this problem instead. Create an orderFactory function that returns an instance of createOrder, which will put the ID number in the scope just over createOrder. That way, the ID will be tracked between multiple calls of createOrder:

    export const orderFactory = (): ((

      color: string,

      size: string,

      qty: number

    ) => Order[]) => {

      let id = 0;

      return (color: string, size: string, qty: number): Order[] => {

        const orders = [];

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

          orders.push({ id: id++, color, size });

        }

        return orders;

      };

    };

    This factory function returns another function, which is defined inline as an arrow function. Before that function is returned, the id variable is declared in the scope just above it. Each invocation of the returned function will see the same instance of id and thus it will retain its value between calls.

  3. In order to make use of the factory, call the function once:

    const createOrder = orderFactory();

    Calling orderFactory once will initialize the ID variable and make it available in the returned function that is now assigned to createOrder. That variable is now enclosed. No other code will be able to access it or, more importantly, modify it.

  4. Run the program and observe that we now get the correct output:

    [

      { id: 0, color: 'red', size: 'M' },

      { id: 1, color: 'red', size: 'M' },

      { id: 2, color: 'red', size: 'M' },

      { id: 3, color: 'red', size: 'M' }

    ]

    [

      { id: 4, color: 'blue', size: 'S' },

      { id: 5, color: 'blue', size: 'S' },

      { id: 6, color: 'blue', size: 'S' },

      { id: 7, color: 'blue', size: 'S' },

      { id: 8, color: 'blue', size: 'S' },

      { id: 9, color: 'blue', size: 'S' },

      { id: 10, color: 'blue', size: 'S' }

    ]

Closures can be very difficult to understand without practice. Beginner TypeScript programmers shouldn't worry about mastering them immediately, but it's very important to recognize factory patterns and the behavior of enclosed variables.

Currying

Currying (named after Haskell Brooks Curry, the mathematician after whom the Haskell, Brooks, and Curry programming languages are also named) is the act of taking a function (or a formula in mathematics) and breaking it down into individual functions, each with a single parameter.

Note

For more information on currying, refer to the following URL: https://javascript.info/currying-partials.

Since functions in TypeScript can return functions, arrow syntax gives us a special concise syntax that makes currying a popular practice. Let's start with a simple function:

Example_Currying_1.ts

1 const addTwoNumbers = (a: number, b: number): number => a + b;

2 console.log(addTwoNumbers(3, 4));

The output is as follows:

7

Here, we've used arrow syntax to describe a function body without braces or the return keyword. The function returns the result of the single expression in the body. This function expects two parameters and can be rewritten as curried functions with a single parameter each:

Example_Currying_2.ts

1 const addTwoNumbers = (a: number): ((b: number) => number) => (b:

2 number): number => a + b;

3 console.log(addTwoNumbers(3)(4));

The output is as follows:

7

This is actually two function declarations. The first function returns another function, which actually does the calculation. Because of closures, the a parameter is available within the second function, as well as its own parameter, b. The two sets of parentheses mean that the first one returns a new function that is then invoked immediately by the second one. The preceding code could be rewritten in a longer form:

Example_Currying_3.ts

1 const addTwoNumbers = (a: number): ((b: number) => number) => {

2 return (b: number): number => {

3 return a + b;

4 }

5 }

6

7 const addFunction = addTwoNumbers(3);

8

9 console.log(addFunction(4));

The output is as follows:

7

It looks a bit silly when written that way, but these do exactly the same thing.

So what use is currying?

Higher-order functions are a variety of curried functions. Higher-order functions both take a function as an argument and return a new function. These functions are often wrapping or modifying some existing functionality. How can we wrap our REST client in a higher-order function to ensure that all responses, whether successful or in error, are handled in a uniform way? This will be the focus of the next exercise.

Exercise 3.06: Refactoring into Curried Functions

Currying makes use of closures and is closely related to the last exercise, so let's return to it and establish the solution from the last exercise as the starting point for this one. Our orderFactory function is doing its job and tracking IDs properly, but the initialization of each type of garment is too slow. The first time an order for red medium comes in, we expect some time will be taken in spinning up this particular recipe, but subsequent red mediums suffer the same latency. Our system isn't efficient enough to handle the demand for popular items. We need some way to cut into the setup time each time a similar order comes in:

Note

The code file for this exercise can be found at https://packt.link/jSKic.

  1. Review the code from Exercise 3.05, Creating the Order Factory with Closures (order-solution.ts):

    interface Order {

      id: number;

      color: string;

      size: string;

    }

    export const orderFactory = (): ((

      color: string,

      size: string,

      qty: number

    ) => Order[]) => {

      let id = 0;

      return (color: string, size: string, qty: number): Order[] => {

        const orders = [];

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

          orders.push({ id: id++, color, size });

        }

        return orders;

      };

    };

    const createOrder = orderFactory();

    const orderOne = createOrder('red', 'M', 4);

    console.log(orderOne);

    const orderTwo = createOrder('blue', 'S', 7);

    console.log(orderTwo);

    How can we use currying to increase efficiency? You need to refactor the code into curried functions.

  2. Refactor orderFactory to return a curried function by breaking up the returned function into three separate functions, each of which returns the next function:

    export const orderFactory = () => {

      let id = 0;

      return (color: string) => (size: string) => (qty: number) => {

        const orders = [];

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

          orders.push({ id: id++, color, size });

        }

        return orders;

      };

    };

    In this case, our refactor is as simple as putting an arrow in between each parameter. Note that this code omits return types from the functions. There are two reasons for this. One is that the type can be reasonably inferred from the code and is quite clear. The other is that adding all of the return types will significantly clutter the code.

    If we add all the return types together, the code will look like this:

    export const orderFactory = (): ((

      color: string

    ) => (size: string) => (qty: number) => Order[]) => {

      let id = 0;

      return (color: string): ((size: string) => (qty: number) => Order[]) => (

        size: string

      ) => (qty: number): Order[] => {

        const orders = [];

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

          orders.push({ id: id++, color, size });

        }

        return orders;

      };

    };

    TypeScript gives us the flexibility of choosing between explicitly declaring types and allowing type inference, when clear, to supply the correct types.

    Now that orderFactory returns a curried function, we can take advantage of it.

  3. Instead of passing every argument to createOrder, call createOrder with just the first argument to establish our line of red garments:

    const redLine = createOrder('red');

  4. Then, further break out the individual items available:

    const redSmall = redLine('S');

    const redMedium = redLine('M');

  5. When necessary or appropriate, create an item on one line:

    const blueSmall = createOrder('blue')('S')

  6. Try creating many different combinations of orders and printing out the results:

    const orderOne = redMedium(4);

    console.log(orderOne);

    const orderTwo = blueSmall(7);

    console.log(orderTwo);

    const orderThree = redSmall(11);

    console.log(orderThree);

  7. When you run the program, you'll see the following output:

    [

      { id: 0, color: 'red', size: 'M' },

      { id: 1, color: 'red', size: 'M' },

      { id: 2, color: 'red', size: 'M' },

      { id: 3, color: 'red', size: 'M' }

    ]

    //...

    Note

    For ease of presentation, only a section of the actual output is shown here.

Currying is a powerful technique for caching variables and partial function results. At this point, we've explored closures, higher-order functions, and currying, all of which show the power and versatility of functions in TypeScript.

Functional Programming

Functional programming is a deep topic and the subject of many books by itself. This book can only touch on the topic. One of the foundational concepts in functional programming is to use simple functions that have an input and an output and do not modify variables that are outside their scope:

Example_Functional_1.ts

1 let importantNumber = 3;

2

3 const addFive = (): void => {

4 importantNumber += 5;

5 };

6

7 addFive();

8

9 console.log(importantNumber);

The function produces the following output:

8

The output of this program is correct. We have indeed added 5 to the initial value of 3, but the addFive method accesses a variable in a higher scope and mutates it. It is greatly preferred in functional programming paradigms to instead return the new value and allow the outer scope to control the variables that have been declared in it. We can change addFive so that it no longer operates on variables outside its scope and instead only operates against its argument and returns the correct value:

Example_Functional_2.ts

1 let importantNumber = 3;

2

3 const addFive = (num: number): number => {

4 return num + 5;

5 };

6

7 importantNumber = addFive(importantNumber);

8

9 console.log(importantNumber);

The function produces the following output:

8

The function is now much more portable. It would be easier to test or reuse since it's not reliant on something in a higher scope. A functional programming paradigm encourages the use of smaller functions. Sometimes, programmers can write functions that do too many different things and are hard to read and maintain. This is often a source of bugs or a negative impact on team velocity. By keeping functions small and simple, we can chain logic together in ways that support maintenance and reusability.

A popular concept in functional programming is immutability. That is the concept whereby once a variable is declared, its value should not change. To understand why this would be a desirable trait, consider a program that has a requirement to print out a customer ID after the customer's name:

Example_Functional_3.ts

1 const customer = {id: 1234, name: 'Amalgamated Materials'}

2

3 const formatForPrint = ()=> {

4 customer.name = `${customer.name} id: ${customer.id}`;

5 };

6

7 formatForPrint();

8

9 console.log(customer.name);

This program does as expected. When the customer's name is printed out, it has the ID behind it; however, we've actually changed the name in the customer object:

Amalgamated Materials id: 1234

What happens If formatForPrint is called repeatedly? With a minor refactor, our code is much safer and more consistent:

const customer = {id: 1234, name: 'Amalgamated Materials'}

const formatForPrint = ()=> {

  return `${customer.name} id: ${customer.id}`;

};

console.log(formatForPrint());

The output is as follows:

Amalgamated Materials id: 1234

It would be even better to pass in the customer object rather than having formatForPrint access it in a higher scope.

TypeScript supports both functional programming and object-oriented paradigms. Many applications borrow from both.

Organizing Functions into Objects and Classes

Sometimes, it makes sense to organize functions into member functions of objects and classes. These concepts will be addressed in greater detail in Chapter 4, Classes and Objects, but for now we can examine how we take a function declaration and add it to an object or class.

Let's take a simple function:

Example_OrganizingFuncs_1.ts

1 function addTwoNumbers(a: number, b: number) { return a + b; }

If we wanted to have an object that contains a number of math functions, we could simply add the following function to it:

2 const mathUtils = {

3 addTwoNumbers

4 };

5

6 console.log(mathUtils.addTwoNumbers(3, 4));

The output is as follows:

7

Note that the syntax used in the mathUtils object is shorthand, meaning the left and right side of the assignment are the same. This could also be written like this:

Example_OrganizingFuncs_2.ts

5 const mathUtils = {

6 addTwoNumbers: addTwoNumbers

7 };

We also have the option of defining the method inline with a function expression:

5 const mathUtils = {

6 addTwoNumbers: function(a: number, b: number) { return a + b; }

7 };

The output in either case will be as follows:

7

Remember that function expressions are usually the best thing to use in objects because they will have the correct this reference. In the case of our mathUtils object, we aren't using the this keyword, so an arrow function could be used, but bear in mind that if, later on, another developer refactors this object, they might not think to change from an arrow function to a function expression and you might wind up with buggy code.

Adding functions to classes can be done in exactly the same way and, in fact, the syntax is very similar. Let's say we want to use a class instead of a plain object and we want to define addTwoNumbers inline. The MathUtils class might look something like this:

class MathUtils {

    addTwoNumbers(a: number, b: number) { return a + b; }

};

Now that we're using a class, in order to call the function, we need to instantiate an object:

const mathUtils = new MathUtils();

console.log(mathUtils.addTwoNumbers(3, 4));

The output is as follows:

7

For more information on classes, see Chapter 4, Classes and Objects.

Exercise 3.07: Refactoring JavaScript into TypeScript

Updating older JavaScript code to TypeScript isn't difficult. If the original code was well written, we can retain much of the structure, but enhance it with interfaces and types. In this exercise, we will use an example legacy JavaScript code that prints the area of various shapes given the dimensions:

Note

The code file for this exercise can be found at https://packt.link/gRVxx.

  1. Start with the following legacy code and make some decisions about what we'd like to improve by converting it to TypeScript:

    var PI = 3.14;

    function getCircleArea(radius) {

      return radius * radius * PI;

    }

    //...

    Note

    Only a section of the actual code is presented here. You can find the complete code at https://packt.link/pahq2.

    A few of the changes are easy. We'll substitute var with const. The functions that determine area are pretty good, but getArea mutates the shape objects. It would be better to just return the area. All of our shapes are pretty well defined, but they would be improved with interfaces.

  2. Let's create some interfaces. Create a new file in VS Code and save it as refactor-shapes-solution.ts.
  3. First, create a Shape interface that includes an enumerated type and an area property. We can extend our Circle, Square, Rectangle, and RightTriangle interfaces from that one:

    const PI = 3.14;

    interface Shape {

      area?: number;

      type: 'circle' | 'rectangle' | 'rightTriangle' | 'square';

    }

    interface Circle extends Shape {

      radius: number;

      type: 'circle';

    }

    interface Rectangle extends Shape {

      length: number;

      type: 'rectangle';

      width: number;

    }

    interface RightTriangle extends Shape {

      base: number;

      height: number;

      type: 'rightTriangle';

    }

    interface Square extends Shape {

      type: 'square';

      width: number;

    }

  4. Now, let's improve and simplify getArea. Instead of accessing properties on each shape, getArea can simply pass the shape to the correct function to determine the area and then return the calculated value:

    const getArea = (shape: Shape) => {

      switch (shape.type) {

        case 'circle':

          return getCircleArea(shape as Circle);

        case 'rectangle':

          return getRectangleArea(shape as Rectangle);

        case 'rightTriangle':

          return getRightTriangleArea(shape as RightTriangle);

        case 'square':

          return getSquareArea(shape as Square);

      }

    };

    This change requires that we make minor changes to all the functions that calculate area.

  5. Instead of each individual property being passed in, now pass in the shape and then grab the props inside the functions:

    const getCircleArea = (circle: Circle): number => {

      const { radius } = circle;

      return radius * radius * PI;

    };

    const getRectangleArea = (rectangle: Rectangle): number => {

      const { length, width } = rectangle;

      return length * width;

    };

    const getSquareArea = (square: Square): number => {

      const { width } = square;

      return getRectangleArea({ length: width, type: 'rectangle', width });

    };

    const getRightTriangleArea = (rightTriangle: RightTriangle): number => {

      const { base, height } = rightTriangle;

      return (base * height) / 2;

    };

    This pattern is very common among modern web app development and works very well in TypeScript development.

  6. Add some type hints to our object declarations:

    const circle: Circle = { radius: 4, type: 'circle' };

    console.log({ ...circle, area: getArea(circle) });

    const rectangle: Rectangle = { type: 'rectangle', length: 7, width: 4 };

    console.log({ ...rectangle, area: getArea(rectangle) });

    const square: Square = { type: 'square', width: 5 };

    console.log({ ...square, area: getArea(square) });

    const rightTriangle: RightTriangle = {

      type: 'rightTriangle',

      base: 9,

      height: 4,

    };

    console.log({ ...rightTriangle, area: getArea(rightTriangle) });

  7. Running the program yields the correct output:

    { radius: 4, type: 'circle', area: 50.24 }

    { type: 'rectangle', length: 7, width: 4, area: 28 }

    { type: 'square', width: 5, area: 25 }

    { type: 'rightTriangle', base: 9, height: 4, area: 18 }

This exercise provided us with practical experience in refactoring legacy JavaScript code into TypeScript. These skills can help us to identify what constituted code quality problems in the original JavaScript code and improve them as we move the code to TypeScript.

Import, Export, and Require

Very small programs, such as the kind often found in books on programming, can work just fine with all the code in a single file. Most of the time, applications will be made up of multiple files, often referred to as modules. Some modules may be dependencies installed from Node Package Manager (npm) and some may be modules you or your team have written. When you look at other projects, you may see the keywords import, export, module, and require used to link different modules together. import and require both serve the same purpose. They allow you to use another module in the module (file) you are currently working in. export and module are the opposite. They allow you to make part or all of your module available for other modules to use.

We'll go over the different syntax options here. The reason for multiple ways to do things has, as usual, to do with the way the languages and runtimes have evolved. Node.js is by far the most popular runtime for server-side JavaScript, and this is where most of our compiled server-side TypeScript will run. Node.js was released in 2009 and, at that time, there was no standard module system for JavaScript. Many JavaScript web applications at that time would simply attach functions and objects to the global window object. This could work fine for web applications, since the window object is refreshed upon loading the page and exists in the web browser, so it's only used by a single user.

Although there is a global object in Node.js, this is not a practical way to link modules together. Doing so would risk one module overwriting another, memory leaks, exposing customer data, and all manner of other catastrophes. The great thing about the module system is that you can share only the bits of your module that you intend to.

Because there was a need for a more robust solution, Node.js adopted the CommonJS spec and the module and require keywords. module is used to share all or part of your module and require is used to consume another module. These keywords were standard in Node.js for many years until ECMAScript 6 introduced the import and export syntax. The latter has been supported in TypeScript for many years and is preferred, although the require syntax is still valid and can be used.

This book will use import and export syntax, as this is standard. The examples that follow will use this syntax, but will also feature the require syntax as a comment so readers can compare.

Any file with the import or export keyword is considered to be a module. Modules may export any variables or functions they declare, either as part of the declaration or by explicitly doing so:

// utils.ts

export const PI = 3.14;

export const addTwoNumbers = (a: number, b: number): number => a + b;

That is equivalent to explicit exports. Here is the complete code for utils.ts:

Example_Import_Exports/utils.ts

1 // utils.ts

2 const PI = 3.14;

3

4 const addTwoNumbers = (a: number, b: number): number => a + b;

5

6 export { PI, addTwoNumbers };

7 // module syntax:

8 // module.exports = { PI, addTwoNumbers };

We can now import our exports into another module (another .ts file – app.ts):

Example_Import_Exports/app.ts

1 // app.ts

2 import { PI, addTwoNumbers } from './utils';

3 // require syntax:

4 // const { PI, addTwoNumbers } = require('./utils');

5 console.log(PI);

6 console.log(addTwoNumbers(3, 4));

Once you run app.ts, you will obtain the following output:

3.14

7

Note

The code files for the preceding example can be found here: https://packt.link/zsCDe

Modules that are part of our application are imported via the relative path from the root of the project. Modules that are imported from our installed dependencies are imported by name. Note that the file extension is not part of the required path, just the filename.

Modules can also have default exports that use the default keyword. Default exports are imported without brackets. Consider the following examples:

Example_Import_Export_2/utils.ts

1 // utils.ts

2 const PI = 3.14;

3 const addTwo = (a: number, b: number): number => {

4 return a + b;

5 };

6 const fetcher = () => {

7 console.log('it is fetched!');

8 };

9 export default { addTwo, fetcher, PI };

The code for app.ts is as follows:

1 // app.ts

2 import utils from './utils';

3 console.log(utils.addTwo(3, 4));

Once you run the app.ts file, you will get the following output:

7

Exercise 3.08: import and export

Looking back at the last exercise, we have a single file that has a bunch of utility functions, and then we have procedural code that establishes some objects, calls the functions, and logs out the output. Let's refactor the result from Exercise 3.07, Refactoring JavaScript into TypeScript to use the import and export keywords and move those functions to a separate module:

Note

The code file for this exercise can be found at https://packt.link/2K4ds. The first step of this exercise requires you to copy-paste some lines of code to your exercise file. Hence, we suggest you either download the code files from this repository or migrate it your desktop before you begin this exercise.

  1. Cut and paste the first 61 lines of shapes.ts into shapes-lib.ts. Your IDE should start warning you that it can no longer find the relevant functions.
  2. Look over the code in shapes-lib.ts. Which functions and interfaces need to be exported? Square, circle, and the rest are utilized directly in shapes.ts, but the shapes interface isn't, so only those four need to be exported. Likewise, the PI constant is only used in shapes-lib.ts, so no need to export that one:

    const PI = 3.14;

    interface Shape {

      area?: number;

      type: 'circle' | 'rectangle' | 'rightTriangle' | 'square';

    }

    export interface Circle extends Shape {

      radius: number;

      type: 'circle';

    }

    export interface Rectangle extends Shape {

      length: number;

      type: 'rectangle';

      width: number;

    }

    export interface RightTriangle extends Shape {

      base: number;

      height: number;

      type: 'rightTriangle';

    }

    export interface Square extends Shape {

      type: 'square';

      width: number;

    }

  3. The only function that needs to be exported is getArea, as that's the only one referenced in shapes.ts:

    export const getArea = (shape: Shape) => {

      switch (shape.type) {

        case 'circle':

          return getCircleArea(shape as Circle);

        case 'rectangle':

          return getRectangleArea(shape as Rectangle);

        case 'rightTriangle':

          return getRightTriangleArea(shape as RightTriangle);

        case 'square':

          return getSquareArea(shape as Square);

      }

    };

  4. Now, let's import the exported interfaces and function into shapes.ts. Your IDE may assist you in this task. For example, in VS Code, if you hover over a module that can be imported, it should ask you whether you'd like to add the import:

    import {

      Circle,

      getArea,

      Rectangle,

      RightTriangle,

      Square,

    } from './shapes-lib-solution';

  5. With all the imports and exports set, run the program again. You should get the correct result:

    { radius: 4, type: 'circle', area: 50.24 }

    { type: 'rectangle', length: 7, width: 4, area: 28 }

    { type: 'square', width: 5, area: 25 }

    { type: 'rightTriangle', base: 9, height: 4, area: 18 }

One of the more challenging things about learning a new programming language is how to structure modules. A good rule of thumb is to always be prepared to break them into smaller chunks if they grow too large. This exercise helps us to understand how we can separate our application logic from utilities or reusable functions, a practice that will lead to clean, maintainable code.

Activity 3.01: Building a Flight Booking System with Functions

As a developer at a start-up for online bookings, you need to implement a system that manages airline bookings. The architecture for this system has already been decided upon. There will be a system for managing flights and seat availability on them and a system for managing bookings. Users will interact directly with the booking system and it, in turn, will search and update flight information.

For the sake of keeping this activity to a manageable size, we'll abstract a number of things, such as customer information, payments, the dates of flights, and even the city of origin. In understanding the problem we need to solve, it can be very helpful to create a diagram describing the flows we need to implement. The following diagram shows the expected workflow for our user:

Note

The code files for this activity can be found here: https://packt.link/o5n0t.

Figure 3.1: Flows that need to be implemented in the flight booking system

Figure 3.1: Flows that need to be implemented in the flight booking system

Here's how the program flows:

  1. Get a list of flights to choose from.
  2. Start a booking with one of those flights.
  3. Pay for the flight.
  4. Complete the booking with seats reserved on the flight.

As the diagram shows, the user will interact with two different systems, a Bookings system and a Flights system. In most scenarios, the user interacts with the Bookings system, but they go directly to the Flights system to search for flights.

In this activity, these systems can be represented by a bookings.ts file and a flights.ts file, which are two TypeScript modules. To complete the activity, implement these two modules in TypeScript. Here are some steps to help you:

  1. Since both the user and the Bookings system depend on the Flights system, start with flights – flights.ts. As the activity is simplified, we can simply return a list of destinations when the user wants to access flights. To allow access to the bookings.ts module, we'll want to use the export keyword on a function.
  2. Although the user has already fetched the flights, we need to check availability before initiating a booking. This is because our system will have many users and availability can change minute by minute. Expose a function for checking availability and another to hold seats while the transaction is completed.
  3. The process payment step really hints at a third system for payments, but we won't include that system in this activity, so just mark the booking as paid when the user gets to the payment step. The Flights system doesn't need to be aware of payment status as that is managed by Bookings.
  4. When we complete the booking, held seats convert to reserved seats. Our booking is finalized and the seats are no longer available on the flight.
  5. A typical output for such an activity would look like this:

    Booked to Lagos {

      bookingNumber: 1,

      flight: {

        destination: 'Lagos',

        flightNumber: 1,

        seatsHeld: 0,

        seatsRemaining: 29,

        time: '5:30'

      },

      paid: true,

      seatsHeld: 0,

      seatsReserved: 1

    //...

    Note

    For ease of presentation, only a part of the actual output is shown here. The solution to this activity can be found via this link.

There are many other scenarios here that could be explored. Try holding all remaining seats, failing to start a new booking for that flight, and then complete the original booking. That should work with the logic we've implemented here! This exercise uses several functions to create a cohesive program. It uses closures, currying, functional programming concepts, and the import and export keywords to share functions between modules.

Unit Testing with ts-jest

Large systems require constant testing to ensure they are correct. This is where unit testing comes in. Some of the biggest software projects in the world have hundreds of millions of lines of code and thousands of features and views. It's simply not possible to manually test every feature. This is where unit tests come in. Unit tests test the smallest unit of code, often a single statement or function, and give us quick feedback if we've done something to change the behavior of an application. Short feedback cycles are a developer's best friend and unit tests are one of the most powerful tools to achieve them.

There are many testing frameworks that can help us to unit test our code. Jest is a popular testing framework from Facebook. You may also come across other frameworks, such as Jasmine, Mocha, or Ava. Jest is a "batteries included" framework that will seem familiar to users of those other frameworks as it has tried to incorporate the best features of all of them.

Jest, Mocha, Ava, and the rest are JavaScript libraries, not TypeScript libraries, and so some special preparation is required to use them. ts-jest is a library that helps us to write TypeScript tests written in TypeScript and to use the Jest test runner and all the good parts of Jest.

To get started, we'll install jest, ts-jest, and typings for jest (@types/jest):

npm install -D jest ts-jest @types/jest

Once the library is installed, we can use npx to initialize ts-jest with a default configuration that will let us write our first test:

npx ts-jest config:init

Running this command will create a config file called jest.config.js. As you become more comfortable writing tests with Jest, you may wish to modify this file, but for now, the default will work just fine.

Some developers put unit tests in a tests directory, and some put the tests directly alongside the source code. Our default Jest config will find both kinds of tests. The convention for unit tests is the name of the module under test, followed by a dot, then the word spec or test, and then the file extension, which will be ts in our case. If we create files with that naming convention anywhere under our project root, Jest will be able to find and execute the tests.

Let's add a simple test. Create a file named example.spec.ts. Then add this code to the file. This code is just a placeholder for the test and doesn't actually do anything other than verify that Jest is working correctly:

describe("test suite for `sentence`", () => {

  test("dummy test", () => {

    expect(true).toBeTruthy();

  });

});

We can run Jest by typing npx jest at the console or we can add an npm script. Try typing npm test at the console. If you haven't changed the default test, you should see something like the following:

npm test

> [email protected] test /Users/mattmorgan/typescript/function-chapter/exercises

> echo "Error: no test specified" && exit 1

Error: no test specified

npm ERR! Test failed. See above for more details.

Let's now update the package.json file so that it runs Jest instead of just failing. Find the package.json file and you'll see this configuration inside it:

  "scripts": {

    "test": "echo "Error: no test specified" && exit 1"

  },

We can replace the entire test with simply jest:

  "scripts": {

    "test": "jest"

  },

Now, try npm test again:

npm test

> [email protected] test /Users/mattmorgan/typescript/function-chapter/exercises

> jest

 PASS ./example.spec.ts

  test suite for `sentence`

    ✓ dummy test (1ms)

Test Suites: 1 passed, 1 total

Tests: 1 passed, 1 total

Snapshots: 0 total

Time: 1.449s

Ran all test suites

Of course, this test doesn't do anything useful. Now, let's import the functions we want to test and write some tests that are actually useful. First, let's clean up the arrow-cat-solution.ts file (from Exercise 3.03, Writing Arrow Functions) a little. We can remove all the console statements because we're going to validate our code by writing tests, not by just logging the console. Then, let's add the export keyword to each of the functions so that our test can import them. arrow-cat-solution.ts now looks like this:

export const arrayToAnd = (words: string[]) => {

  return words.reduce((prev, curr, index) => {

    if (words.length === 1) {

      return ` ${curr}`;

    }

    if (words.length - 1 === index) {

      return `${prev} and ${curr}`;

    }

    return `${prev} ${curr},`;

  }, "");

};

export const capitalize = (sentence: string) => {

  return `${sentence.charAt(0).toUpperCase()}${sentence

    .slice(1)

    .toLowerCase()}`;

};

export const sentence = (

  subject: string,

  verb: string,

  ...objects: string[]

): string => {

  return capitalize(`${subject} ${verb}${arrayToAnd(objects)}.`);

};

Let's try writing a test for the capitalize function. We simply need to call the function and test the outcome against the expected outcome. First, import the function in a new file (arrow-cat-solution.spec.ts):

import { capitalize } from './arrow-cat-solution';

Then, write an expectation. We expect our function to turn all-caps "HELLO" into "Hello". Let's now write that test and execute it:

describe("test suite for `sentence`", () => {

  test("capitalize", () => {

    expect(capitalize("HELLO")).toBe("Hello");

  });

});

Did it work?

npm test

> [email protected] test /Users/mattmorgan/typescript/function-chapter/exercises

> jest

 PASS ./example.spec.ts

  test suite for `sentence`

    ✓ capitalize (1ms)

Test Suites: 1 passed, 1 total

Tests: 1 passed, 1 total

Snapshots: 0 total

Time: 0.502s, estimated 2s

Ran all test suites.

The describe keyword is used to group tests and its only purpose is to affect the output of your test report. The test keyword should wrap the actual test. Instead of test, you can write it. Tests that use it are often written as an assertion with should:

  it("should capitalize the string", () => {

    expect(capitalize("HELLO")).toBe("Hello");

  });

Now, write tests for the other functions.

Activity 3.02: Writing Unit Tests

In the last activity, we built a booking system for airlines and applied TypeScript functions to the scenarios involved in securing a flight reservation. We executed these scenarios from a single index.ts file, representing user interactions. This approach works well enough while we're learning, but it's a bit messy and doesn't actually assert that any of the scenarios are correct. To put that another way, it's almost a unit test, but it's not as good as a unit test.

We've learned about how to install Jest, so let's use it to unit test Activity 3.01, Building a Flight Booking System with Functions. For each function we wrote, we'll write a test that invokes the function and tests the output:

Note

The code files for this activity can be found at https://packt.link/XMOZO.

  1. The code stubs provided for this activity include bookings.test.ts and flights.test.ts with a number of unimplemented tests. Implement those tests to complete this activity.

    You can execute the tests by running npm test. You can also run just the solutions with npm run test:solution.

  2. To test a function, you will need to import it into your test file.
  3. Invoke the function with sample input, and then use Jest's expect assertions to test the output, for example, expect(value).toBe(5);.
  4. Error scenarios can be tested with try/catch blocks, catching the error thrown by the function, and then testing the error condition. When using catch in a unit test, it's a best practice to use expect.assertions to indicate how many assertions you want to test. Otherwise, your test might complete without the catch block being invoked.
  5. Try to reach 100% line coverage in the coverage report (already configured with --coverage).

    Note

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

In this activity, we took a program we'd written and applied best practices with some good unit tests. It will now be much easier to add additional functionality and scenarios knowing that the existing code is tested. Instead of writing out an index file to call various functions, we now have things logically grouped, ordered, and tested. We have a mechanism to track line coverage and understand how much of our code is under test.

Error Handling

When we write functions, we need to bear in mind that not everything always works perfectly. What will we do if the function receives unexpected input? How will our program react if some other function that we need to call doesn't work perfectly? It's always a good idea to validate function input. Yes, we're using TypeScript, and we can be reasonably sure that if we expect a string, we won't get an object instead, but sometimes, external input doesn't conform to our types. Sometimes, our own logic may be erroneous. Consider this function:

const divide = (numerator: number, denominator: number) => {

    return numerator / denominator;

}

It looks fine, but what if I pass in the number 0 as the denominator? We cannot divide by zero, and so the result will be the constant, NaN. NaN, when used in any mathematical equation, will always return NaN. This could introduce a serious bug into our system, and this needs to be avoided.

To solve this problem, we need to figure out what should happen if we get invalid input. Log it? Throw an error? Just return zero? Exit the program? Once that is decided, we can add some validation to our function:

const divide = (numerator: number, denominator: number) => {

    if(denominator === 0) {

        throw 'Cannot divide by zero!'

    }

    return numerator / denominator;

}

Now at least we won't fail silently as we are displaying a warning on the screen, Cannot divide by zero!. It's always better to raise an exception than for a function to fail without anybody noticing.

Summary

By now, you know how to create the most important building blocks of any TypeScript program – functions. We have explored the difference between function expressions and arrow functions and when to use which. We looked at immediately invoked function expressions, closures, currying, and other powerful TypeScript techniques.

We talked about functional programming paradigms and looked at how to include functions in objects and classes. We've looked at how to convert legacy JavaScript code into modern TypeScript and how we can improve our software by doing so.

We have had an overview of the TypeScript module system and the critically important import and export keywords. We wrote a lot of our own TypeScript code and learned how to test it with ts-jest.

Finally, we rounded out this chapter with a discussion of error handling. We'll look at more advanced error-handling techniques in Chapters 12, Guide to Promises in TypeScript, and Chapter 13, Async Await in TypeScript, when it comes to asynchronous programming.

We covered quite a few topics in this chapter, and most readers won't retain all of them immediately. That's OK! You have written a number of functions in this chapter and you'll write many more in chapters to come. Writing good functions is a skill that comes with practice and you'll be able to refer back to this chapter to check your learning as you progress in your mastery of TypeScript.

In the next chapter, we will further explore the object-oriented programming paradigm by studying the class keyword and how we can construct type-safe objects.

..................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.178