11. Higher-Order Functions and Callbacks

Overview

This chapter introduces higher-order functions and callbacks in TypeScript. You will first understand what higher-order functions are, why they are useful, and how to type them correctly in TypeScript. Then, the chapter will teach you about what callbacks are, why they are used, and in what situations. You will also learn about why callbacks are so widely used, especially in Node.js.

Additionally, the chapter will provide you with a basic introduction to the event loop. Not only will you learn about "callback hell," but also how you can avoid it. By the end of this chapter, you will be able to create a well-typed higher-order pipe function.

Introduction

You have already covered the use of functions in TypeScript in Chapter 3, Functions. This chapter will introduce you to higher-order functions in TypeScript. Hitherto, with all the functions that you have used in this book, you either passed parameters or arguments into them. However, JavaScript and, by extension, TypeScript, has many ways of composing and writing code. In this chapter, we'll explore one such pattern – higher-order functions/callbacks (hereinafter called HOCs) are functions that either take in another function as an argument or return a function (or both).

Additionally, this chapter also explores the concept of callbacks. Callbacks are required in Node.js, as well as in other JavaScript runtimes, since the language is single-threaded and runs in an event loop, and so, in order to not hold up the main thread, we let other code run, and when needed it will call our code back. This chapter will also touch upon "callback hell" and equip you with the skills needed to avoid it.

Introduction to HOCs – Examples

HOCs are frequently used in JavaScript, and especially in Node.js, where even the simplest backend server application contains it. Here is an example:

const http = require("http");

http.createServer((req, res) => {

  res.write("Hello World");

  res.end();

}).listen(3000, () => {

  console.log("? running on port 3000");

});

Notice that the createServer function takes in a request listener function, which will be used to handle any incoming requests. This function will take in two arguments, req and res – the request object and the response object, respectively:

Figure 11.1: Part of the http module in Node.js describing the callback 
structure of RequestListener

Figure 11.1: Part of the http module in Node.js describing the callback structure of RequestListener

In addition, the listen method also takes in an optional function that will run when the server is ready to listen for requests.

Both createServer and listen are HOCs because they take in functions as arguments. These argument functions are usually called callbacks, since this is how our code can get "called back" (notified) when something happens, and, if needed, handle it appropriately. In the preceding example, the HTTP server needs to know how to handle incoming requests, so it calls our given requestListener function, which provides the logic for that. Later, the listen function wants to let us know when it's ready to accept requests, and it calls our given callback when it is.

Another example is the setTimeout function, which takes in another function as an argument to call later – after the timeout has passed:

setTimeout(() => {

    console.log('5 seconds have passed');

}, 5000);

function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timeout;

Another example of an HOC that does not take a callback function is the memoize function. This takes in a function to memoize as an argument and returns a function with the same signature:

function memoize<Fn extends AnyFunction>(fn: Fn, keyGetter?: KeyGetter<Fn>): Fn;

Note

The memoize function takes in a function and returns a function with the same type signature; however, the returned function caches the results of the original function. This is usually useful for expensive functions that take a long time to run and return the same output for the same arguments. Chapter 9, Generics and Conditional Types, Exercise 9.01, implements such a memoize function.

In the following sections, we'll explore both kinds of HOCs in more detail and see how we can avoid some of the pitfalls they introduce.

Higher-Order Functions

Higher-order functions are regular functions that follow at least one of these two principles:

  1. They take one or more functions as arguments.
  2. They return a function as a result.

For example, let's say we want to write a greet function:

Example01.ts

1 function greet(name: string) {

2 console.log(`Hello ${name}`);

3 }

4

5 greet('John');

The following is the output:

Hello John

This is a fine function, but it's very limited – what if each person has a favorite greeting? Consider the following example:

Example02.ts

1 const favoriteGreetings: Record<string, string> = {

2 John: 'Hey',

3 Jane: 'Hello',

4 Doug: 'Howdy',

5 Sally: 'Hey there',

6 };

We could put that inside the greet function:

function greet(name: string) {

  const greeting = favoriteGreetings[name] || 'Hello';

  console.log(`${greeting} ${name}`);

}

greet('John');

The following is the output:

Hey John

But that means that the greet function is no longer reusable by itself since, if we were to take it, we'd also need to bring along the favoriteGreetings mapping with it. Instead, we could pass it in as a parameter:

Example03.ts

1 function greet(name: string, mapper: Record<string, string>) {

2 const greeting = mapper[name] || 'Hello';

3 console.log(`${greeting} ${name}`);

4 }

5

6 greet('John', favoriteGreetings); // prints 'Hey John'

7 greet('Doug', favoriteGreetings); // prints 'Howdy Doug'

The following is the output:

Hey John

Howdy Doug

This works, but it's very cumbersome to pass in the favoriteGreetings object in every call.

We can improve on this by making the greet function accept a function that will serve as a more generic solution to the favorite-greeting issue – it will accept the name and return the greeting to use:

Example04.ts

1 function greet(name: string, getGreeting: (name: string) => string) {

2 const greeting = getGreeting(name);

3 console.log(`${greeting} ${name}`);

4 }

5

6 function getGreeting(name: string) {

7 const greeting = favoriteGreetings[name];

8 return greeting || 'Hello';

9 }

10

11 greet('John', getGreeting); // prints 'Hey John'

12 greet('Doug', getGreeting); // prints 'Howdy Doug'

The following is the output:

Hey John

Howdy Doug

This may feel the same as our previous solution, which took the mapper object as an argument, but passing in a function is much more powerful. We can do a lot more with a function than with a static object. For example, we could base the greeting on the time of day:

Example05.ts

1 function getGreeting(name: string) {

2 const hours = new Date().getHours();

3 if (hours < 12) {

4 return 'Good morning';

5 }

6

7 if (hours === 12) {

8 return 'Good noon';

9 }

10

11 if (hours < 18) {

12 return 'Good afternoon';

13 }

14

15 return 'Good night';

16 }

17

18 greet('John', getGreeting); // prints 'Good morning John' if it's morning

19 greet('Doug', getGreeting); // prints 'Good morning Doug' if it's morning

An example output would be as follows:

Good afternoon John

Good afternoon Doug

We could even go further and make the function return a random greeting, get it from a remote server, and a lot more, something which we couldn't do without passing in a function to the greet function.

By making greet accept a function, we opened up endless possibilities, while keeping greet reusable.

This is great, but passing in the getGreeting function in each call still feels cumbersome. We can change this by changing the greet function to both accept a function, and return a function. Let's take a look at how that appears:

Example06.ts

1 function greet(getGreeting: (name: string) => string) {

2 return function(name: string) {

3 const greeting = getGreeting(name);

4 console.log(`${greeting} ${name}`);

5 };

6 }

You'll notice that the logic stays the same as in the previous solution, but we split up the function to first take in the getGreeting function, and then return another function that takes in the name argument. This allows us to call greet like so:

const greetWithTime = greet(getGreeting);

greetWithTime('John'); // prints 'Good morning John' if it's morning

greetWithTime('Doug'); // prints 'Good morning Doug' if it's morning

Splitting greet in this way allows us even more flexibility – since we now only need the getGreeting function once we can inline it, if it doesn't make sense to use it elsewhere:

8 const greetWithTime = greet(function(name: string) {

9 const hours = new Date().getHours();

10 if (hours < 12) {

11 return 'Good morning';

12 }

13

14 if (hours === 12) {

15 return 'Good noon';

16 }

17

18 if (hours < 18) {

19 return 'Good afternoon';

20 }

21

22 return 'Good night';

23 });

We could also use it to greet an array of people (names), using the forEach method of Array:

const names = ['John', 'Jane', 'Doug', 'Sally'];

names.forEach(greetWithTime);

The following is the output:

Good afternoon John

Good afternoon Jane

Good afternoon Doug

Good afternoon Sally

Higher-order functions, especially ones that accept other functions, are very widespread and useful, especially for manipulating datasets. We've even used them in previous chapters. For instance, the map, filter, reduce, and forEach methods of Array accept functions as arguments.

Exercise 11.01: Orchestrating Data Filtering and Manipulation Using Higher-Order Functions

In this exercise, we get a list of students and want to get the average score of the students who graduated in 2010. This exercise will make use of higher-order functions to complete this task.

The list of students is given in the following form:

interface Student {

  id: number;

  firstName: string;

  lastName: string;

  graduationYear: number;

  score: number;

}

const students: Student[] = [

  { id: 1, firstName: 'Carma', lastName: 'Atwel', graduationYear: 2010, score: 88 },

  { id: 2, firstName: 'Shaun', lastName: 'Knoller', graduationYear: 2011, score: 84 },

  // ...

];

Note

You can refer to the following starter file to get the code for student interface: https://packt.link/6Jmeu.

Perform the following steps to implement this exercise:

Note

The code file for this exercise can be found here: https://packt.link/fm3O4. Make sure to begin with the code for the student interface, as mentioned previously.

  1. Create a function, getAverageScore, that will accept a Student[] argument, and return a number:

    function getAverageScoreOf2010Students(students: Student[]): number {

      // TODO: implement

    }

  2. First, we want to get only those students who graduated in 2010. We can use the array's filter method for that – a higher-order function that accepts a predicate, a function that accepts an item from the array and returns true or false, depending on whether the item should be included in the result. filter returns a new array comprising some of the original array items, depending on the predicate. The length of the new array is smaller or equal to the length of the original array.
  3. Update your function with the following code:

    function getAverageScoreOf2010Students(students: Student[]): number {

      const relevantStudents = students.filter(student => student.graduationYear === 2010);

    }

    Next, we only care about the score of each student. We can use the array's map method for that – a higher-order function that accepts a mapping function, a function that accepts an item from the array and returns a new, transformed value (of a type of your choosing) for each item. map returns a new array comprising the transformed items.

  4. Use the map method as shown:

    function getAverageScoreOf2010Students(students: Student[]): number {

      const relevantStudents = students.filter(student => student.graduationYear === 2010);

      const relevantStudentsScores = relevantStudents.map(student => student.score);

    }

    Lastly, we want to get the average from the array of scores. We can use the array's reduce method for that – a higher-order function that accepts a reducer function and an initial value.

  5. Update the function with the reduce method as shown:

    function getAverageScoreOf2010Students(students: Student[]): number {

      const relevantStudents = students.filter(student => student.graduationYear === 2010);

      const relevantStudentsScores = relevantStudents.map(student => student.score);

      const relevantStudentsTotalScore = relevantStudentsScores.reduce((acc, item) => acc + item, 0);

      return relevantStudentsTotalScore / relevantStudentsScores.length;

    }

    The reducer function accepts the accumulator and the current value and returns an accumulator. reduce iterates over the items in the array, calling the reducer function in each iteration with the current item and the previously returned accumulator (or the initial value, for the first run). Finally, it returns the resulting accumulator, after iterating through the entire array. In this case, we want to average out the numbers in the array, so our reducer function will sum all the items, which we'll then divide by the number of female students. We can then call the function with any dataset and get the average score.

  6. Run the file using npx ts-node. You should see the following output on your console:

    The average score of students who graduated in 2010 is: 78.5

    Note

    In this exercise, we could also extract each function given to filter, map, and reduce into a named, non-inlined function, if it made sense to use it outside of this context; for example, if we wanted to test the filtering logic outside of getAverageScoreOf2010Students.

Callbacks

Callbacks are functions that we pass into other functions, which, in turn, will be invoked when they are needed. For example, in the client, if you want to listen to clicks on a specific DOM element, you attach an event handler via addEventListener. The function you pass in is then called when clicks on that element occur:

const btnElement = document.querySelector<HTMLButtonElement>('.my-button');

function handleButtonClick(event: MouseEvent) {

  console.log('.my-button was clicked!');

}

btnElement.addEventListener('click', handleButtonClick);

In this example, handleButtonClick is a callback function given to addEventListener. It will be called whenever someone clicks the .my-button element.

Note

You can also inline the handleButtonClick function, but you won't be able to call removeEventListener later, which is required in certain cases, to avoid memory leaks.

On the server, callbacks are widely used. Even the most basic request handler in Node.js' http module requires a callback function to be passed:

import http from 'http';

function requestHandler(req: http.IncomingMessage, res: http.ServerResponse) {

  res.write('Hello from request handler');

  res.end();

}

http

  .createServer(requestHandler)

  .listen(3000);

In this example, requestHandler is a callback function given to createServer. It will be called whenever a request reaches the server, and this is where we define what we want to do with it, and how we want to respond.

The Event Loop

Since JavaScript is single-threaded, callbacks are required to keep the main thread free – the basic idea is that you give the engine a function to call when something happens, where you can handle it, and then return the control to whatever other code needs to run.

Note

In more recent versions of browsers and Node.js, you can create threads via Web Workers on the browser or via Worker Threads in Node.js. However, these are usually saved for CPU-intensive tasks, and they are not as easy to use as callbacks or other alternatives are (for example, Promises – explored in more detail in Chapter 13, Async Await in TypeScript).

To illustrate this, let's look at a version of some JavaScript code where there are no callbacks, and we want to create a simple server that greets the users by their name:

// server.ts

function logWithTime(message: string) {

  console.log(`[${new Date().toISOString()}]: ${message}`);

}

http

  .createServer((req, res) => {

    logWithTime(`START: ${req.url}`);

    const name = req.url!.split('/')[1]!;

    const greeting = fetchGreeting(name);

    res.write(greeting);

    res.end();

    logWithTime(`END: ${req.url}`);

  })

  .listen(3000);

fetchGreeting is faking a network operation, which is done synchronously to illustrate the issue:

function fetchGreeting(name: string) {

  const now = Date.now();

  const fakeRequestTime = 5000;

  logWithTime(`START: fetchGreeting for user: ${name}`);

  

  while (Date.now() < now + fakeRequestTime);

  

  logWithTime(`END: fetchGreeting for user: ${name}`);

  return `Hello ${name}`;

}

In a more real-world example, fetchGreening could be replaced by a call to get the user's data from the database.

If we run the server and try to request a few greetings simultaneously, you'll notice that they each wait for the previous request to complete before starting requesting the data for the current one. We can simulate a few concurrent requests by calling fetch multiple times, without waiting for the previous request to finish first:

// client.ts

fetch('http://localhost:3000/john');

fetch('http://localhost:3000/jane');

The output you'll see on the server's console is this:

Figure 11.2: Output of running the sync server while making multiple requests simultaneously

Figure 11.2: Output of running the sync server while making multiple requests simultaneously

As you can see, Jane had to wait for John's request to finish (5 seconds in this case) before the server even started handling her request. The total time to greet both users was 10 seconds. Can you imagine what would happen in a real server, serving hundreds or more requests at the same time?

Let's see how callbacks solve this.

We first change fetchGreeting to use callback APIs – setTimeout in this case serves the same purpose as the while loop from before, while not holding up the main thread:

function fetchGreeting(name: string, cb: (greeting: string) => void) {

  const fakeRequestTime = 5000;

  logWithTime(`START: fetchGreeting for user: ${name}`);

  setTimeout(() => {

    logWithTime(`fetched greeting for user: ${name}`);

    cb(`Hello ${name}`);

  }, fakeRequestTime);

  logWithTime(`END: fetchGreeting for user: ${name}`);

}

Then, change the request handler to use the new implementation:

// server.ts

http

  .createServer((req, res) => {

    logWithTime(`START: ${req.url}`);

    const name = req.url!.split('/')[1]!;

    fetchGreeting(name, greeting => {

      logWithTime(`START: callback for ${name}`);

      res.write(greeting);

      res.end();

      logWithTime(`END: callback for ${name}`);

    });

    logWithTime(`END: ${req.url}`);

  })

  .listen(3000);

And run the client code again. This results in the following output:

Figure 11.3: Output of running the async server while making 
multiple requests simultaneously

Figure 11.3: Output of running the async server while making multiple requests simultaneously

As you can see, the server started handing John's request first, since that's the first one to arrive, but then immediately switched to handling Jane's request while waiting for John's greeting to be ready. When John's greeting was ready 5 seconds later, the server sent the greeting back, and then waited for Jane's greeting to be ready a few milliseconds later and sent that to her.

To conclude, the same flow as before now took 5 seconds to respond to both users instead of the 10 seconds from before. In addition, most of that time was spent idle – waiting to receive more requests to handle. This is instead of the flow prior to the callbacks, where the server was stuck and wasn't able to answer any requests for the majority of the time.

Callbacks in Node.js

Since callbacks are very common in Node.js, and especially since the whole ecosystem relies on using external packages for a lot of things, there is a standard callback API structure for any async function:

  1. The callback function will be the last parameter.
  2. The callback function will take err as the first parameter, which may be null (or undefined), and the response data as the second parameter.

Further parameters are also allowed, but these two are mandatory. This results in a predictable structure for handling callbacks, illustrated by the following example for reading a file from the filesystem:

import fs from "fs";

fs.readFile("some-file", (err, file) => {

  if (err) {

    // handle error...

    return;

  }

  // handle file...

});

Callback Hell

Unfortunately, code that uses callbacks can make it very hard to follow, understand, and reason about very quickly. Every async operation requires another callback level, and if you want to run multiple async operations consecutively, you have to nest these callbacks.

For example, let's say we want to build a social network, which has an endpoint where you can ask for a given user's friends, based on their username. Getting this list of friends requires multiple operations, each requiring an async operation that depends on the result of the previous one:

  1. Get the requested user's ID from the database (given their username).
  2. Get the privacy settings of the user to make sure they allow others to view their list of friends.
  3. Get the user's friends (from an external service or otherwise).

Here is some example code for how this could be done, using callbacks. We're using express here to set up a basic server, listening on port 3000. The server can accept a GET request to /:username/friends (where :username will be replaced with the actual requested username). After accepting the request, we get the ID of the user from the database, then get the user privacy preferences using the user's ID (this can be in an external service, or otherwise) to check that they allow others to view their friends' list, then get the user's friends, and finally return the result:

import express from 'express';

import request from 'request';

import sqlite from 'sqlite3';

const db = new sqlite.Database('db.sql', err => {

  if (err) {

    console.error('Error opening database:', err.message);

  }

});

const app = express();

app.get('/:username/friends', (req, res) => {

  const username = req.params.username;

  db.get(

    `SELECT id

    FROM users

    WHERE username = username`,

    [username],

    (err, row) => {

      if (err) {

        return res.status(500).end();

      }

      getUserPrivacyPreferences(row.id, (err, privacyPreferences) => {

        if (err) {

          return res.status(500).end();

        }

        if (!privacyPreferences.canOthersViewFriends) {

          return res.status(403).end();

        }

        getFriends(row.id, (err, friends) => {

          if (err) {

            return res.status(500).end();

          }

          return res

            .status(200)

            .send({ friends })

            .end();

        });

      });

    }

  );

});

app.get('*', (req, res) => {

  res.sendFile('index.html');

});

app.listen(3000);

Also note that in each callback, we got an err parameter and had to check whether it was true, and bail early if it wasn't accompanied by an appropriate error code.

The preceding example is not unrealistic, and a lot of cases require more levels than this to get all the data they need in order to perform a task. And so, this "callback hell" becomes more apparent, and harder to understand and reason about very quickly, since, as discussed previously, a lot of APIs in Node.js work with callbacks, due to the nature of how JavaScript works, as explained in the event loop section.

Avoiding Callback Hell

There are quite a few solutions to the callback hell problem. We'll take a look at the most prominent ones, demonstrating how the preceding code snippet would look in each variation:

  1. Extract the callback functions to function declarations at the file level and then use them – this means you only have one level of functions with business logic, and the callback hell functions become a lot shorter.
  2. Use a higher-order function to chain the callbacks, meaning only a single level of callbacks in practice.
  3. Use promises, when can be chained together, as explained in Chapter 13, Async Await in TypeScript.
  4. Use async/await (which is syntactic sugar on top of Promise), as explained in Chapter 13, Async Await in TypeScript.

Splitting the Callback Handlers into Function Declarations at the File Level

The simplest way to simplify callback hell is to extract some of the callbacks into their own top-level functions and let each one call the next in the logical chain.

Our main endpoint handler will call the get of db as before, but then just call handleDatabaseResponse with the response, leaving it to handle any errors, and so on. This is why we also pass in the response object to the function, in case it needs to return the data, or an error, to the user:

app.get('/:username/friends', (req, res) => {

  const username = req.params.username;

  db.get(

    `SELECT id

    FROM users

    WHERE username = username`,

    [username],

    (err, row) => {

      handleDatabaseResponse(res, err, row);

    }

  );

});

The handleDatabaseResponse function will perform the same logic as before, but now pass the handling of the getUserPrivacyPreferences response to handleGetUserPrivacyPreferences:

function handleDatabaseResponse(res: express.Response, err: any, row: { id: string }) {

  if (err) {

    return res.status(500).end();

  }

  getUserPrivacyPreferences(row.id, (err, privacyPreferences) => {

    handleGetUserPrivacyPreferences(res, row.id, err, privacyPreferences);

  });

}

handleGetUserPrivacyPreferences will again perform the same logic as before, and pass the handling of the getFriends response to handleGetFriends:

function handleGetUserPrivacyPreferences(

  res: express.Response,

  userId: string,

  err: any,

  privacyPreferences: PrivacyPreferences

) {

  if (err) {

    return res.status(500).end();

  }

  if (!privacyPreferences.canOthersViewFriends) {

    return res.status(403).end();

  }

  getFriends(userId, (err, friends) => handleGetFriends(res, err, friends));

}

And finally, handleGetFriends will return the data to the user via the response:

function handleGetFriends(res: express.Response, err: any, friends: any[]) {

  if (err) {

    return res.status(500).end();

  }

  return res

    .status(200)

    .send({ friends })

    .end();

}

Now we only have a single nesting level, and no more callback hell.

The main trade-off here is that while the code is less nested, it is split among multiple functions and may be harder to follow, especially when debugging or skimming through it to understand what's going on at a high level.

Chaining Callbacks

There are libraries to help us eliminate the callback hell problem by chaining the callbacks to one another – artificially removing nesting levels from our code. One of the popular ones is async.js (https://github.com/caolan/async), which exposes a few functions to compose callback functions, such as parallel, series, and waterfall. In our preceding code example, we could use the waterfall function to chain the callbacks to happen one after the other:

  1. We implement an array of functions, and a final handler. async will then call our functions, one by one, when we call the callback in each function, as demonstrated here:

    ...

    import async from 'async';

    ...

    type CallbackFn = <T extends any[]>(err: any, ...data: T) => void;

    class ServerError extends Error {

      constructor(public readonly statusCode: number, message?: string) {

        super(message);

      }

    }

    app.get('/:username/friends', (req, res) => {

      const username = req.params.username;

  2. Get the user ID from the database:

      async.waterfall(

        [

          // 1. Get the user id from the database

          (cb: CallbackFn) => {

            db.get(

              `SELECT id

                FROM users

                WHERE username = username`,

              [username],

              (err, row) => {

                if (err) {

                  return cb(err);

                }

                return cb(null, row);

              }

            );

          },

  3. Get the user's privacy settings:

          (row: { id: string }, cb: CallbackFn) => {

            getUserPrivacyPreferences(row.id, (err, privacyPreferences) => {

              if (err) {

                return cb(err);

              }

              return cb(null, privacyPreferences, row.id);

            });

          },

  4. Check that the user privacy settings allow others to view their friends:

          (privacyPreferences: PrivacyPreferences, userId: string, cb: CallbackFn) => {

            if (!privacyPreferences.canOthersViewFriends) {

              return cb(new ServerError(403, "User doesn't allow others to view their friends"));

            }

            return cb(null, userId);

          },

  5. Get the user's friends:

          (userId: string, cb: CallbackFn) => {

            getFriends(userId, (err, friends) => {

              if (err) {

                return cb(err);

              }

              return cb(null, friends);

            });

          },

        ],

  6. Finally, handle any errors that occurred, or the data that was returned from the last callback:

        (error, friends) => {

          if (error) {

            if (error instanceof ServerError) {

              return res

                .status(error.statusCode)

                .send({ message: error.message })

                .end();

            }

            return res.status(500).end();

          }

          return res

            .status(200)

            .send({ friends })

            .end();

        }

      );

    });

Now the code is much easier to follow – we only have one error handler that's tied down to the response object, and we follow the code from top to bottom, with not much nesting in between, at least not due to callbacks.

Promises

Promises allow you to essentially flatten the callback tree by doing something similar to async.js' waterfall, but it's more seamless, built into the language itself, and also allows promises to be "squashed."

We won't go into too much detail here – refer to Chapter 13, Async Await in TypeScript for an in-depth explanation of promises, but the preceding code, using promises, would look like this:

...

app.get('/:username/friends', (req, res) => {

  const username = req.params.username;

  promisify<string, string[], { id: string }>(db.get)(

    `SELECT id

  FROM users

  WHERE username = username`,

    [username]

  )

    .then(row => {

      return getUserPrivacyPreferences(row.id).then(privacyPreferences => {

        if (!privacyPreferences.canOthersViewFriends) {

          throw new ServerError(403, "User doesn't allow others to view their friends");

        }

        return row.id;

      });

    })

    .then(userId => {

      return getFriends(userId);

    })

    .then(friends => {

      return res

        .status(200)

        .send({ friends })

        .end();

    })

    .catch(error => {

      if (error instanceof ServerError) {

        return res

          .status(error.statusCode)

          .send({ message: error.message })

          .end();

      }

      return res.status(500).end();

    });

});

async/await

Async/await builds upon promises and provides further syntactic sugar on top of them in order to make promises look and read like synchronous code, even though, behind the scenes, it's still async. You can get a more in-depth explanation of them in Chapter 13, Async Await in TypeScript, but the preceding code that used promises is equivalent to the following code that uses async/await:

...

app.get('/:username/friends', async (req, res) => {

  const username = req.params.username;

  try {

    const row = await promisify<string, string[], { id: string }>(db.get)(

      `SELECT id

       FROM users

       WHERE username = username`,

      [username]

    );

    const privacyPreferences = await getUserPrivacyPreferences(row.id);

    if (!privacyPreferences.canOthersViewFriends) {

      throw new ServerError(403, "User doesn't allow others to view their friends");

    }

    const friends = await getFriends(row.id);

    return res

      .status(200)

      .send({ friends })

      .end();

  } catch (error) {

    if (error instanceof ServerError) {

      return res

        .status(error.statusCode)

        .send({ message: error.message })

        .end();

    }

    return res.status(500).end();

  }

});

Activity 11.01: Higher-Order Pipe Function

In this activity, you are tasked with implementing a pipe function – a higher-order function that accepts other functions, as well as a value, and composes them – returning a function that accepts the arguments of the first function in the composition, runs it through the functions – feeding each function with the output of the previous one (and the first function with the initial value), and returns the result of the last function.

Such functions exist in utility libraries such as Ramda (https://ramdajs.com/docs/#pipe). and with variations in other libraries such as Lodash (https://lodash.com/docs#chain) and RxJS (https://rxjs.dev/api/index/function/pipe).

Note

You can find both the activity starter file and solution at https://packt.link/CQLfx.

Perform the following steps to implement this activity:

  1. Create a pipe function that accepts functions as arguments and composes them, from left to right.
  2. Make sure that the return type of the returned functional is correct – it should accept arguments of type T, T being the arguments of the first function in the chain, and return type R, R being the return type of the last function in the chain.

    Note that due to a current TypeScript limitation, you have to manually type this for the number of arguments you want to support.

  3. Your pipe function should be callable in multiple ways – supporting composition of up to five functions, and will only support composing functions with a single argument, for simplicity.

    Here is the structure of the pipe function that you can use:

    const func = pipe(

      (x: string) => x.toUpperCase(),

      x => [x, x].join(','),

      x => x.length,

      x => x.toString(),

      x => Number(x),

    );

    console.log('result is:', func('hello'));

After solving the preceding steps, the expected output for this code is presented here:

result is: 11

Bonus: As a challenge, try expanding the pipe function to support the composition of more functions, or more arguments.

Note

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

Summary

In this chapter, we introduced two key concepts in TypeScript – higher-order functions and callbacks. The chapter first defined HOCs and illustrated this concept with a number of examples. You also orchestrated data filtering and manipulation using higher-order functions. Finally, you also tested your skills by creating a higher-order pipe function.

With regard to callbacks, the chapter first introduced the definition of callbacks with a few generic examples, along with examples relating to callbacks in Node.js. You also saw how you can easily fall into callback hell and how you can avoid it. Although there are several additional steps that you need to take in order to master higher-order functions and callback, this chapter got you started on the journey. The next chapter deals with another important concept in TypeScript – promises.

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

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