Wanna see something cool? Check out Angular Spotify 🎧

Apply types to entire function expressions when possible

From Effective TypeScript: 62 Specific Ways to Improve Your TypeScript by Dan Vanderkam

It is a really good book. Please purchase the book to support its author!

If you are the publisher and think this article should not be public, please write me an email to trungk18 [at] gmail [dot] com and I will make it private.

JavaScript (JS) and TypeScript (TS) distinguishes a function statement and a function expression:

function rollDice1(sides: number): number { return 0;}  // Statement
const rollDice2 = function(sides: number): number { return 0;};  // Expression
const rollDice3 = (sides: number): number => { return 0;};  // Also expression

An advantage of function expressions in TS is that you can apply a type declaration to the entire function at one, rather than specifying the types of the parameters and return type individually.

type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { return 0; };

If you mouse over sides in your editor, you’ll see that TypeScript knows its type is number. The function type doesn’t provide much value in such a simple example, but the technique does open up a number of possibilities.

1. Reducing repetition

If you wanted to write several functions for doing arithmetic on numbers, for instance, you could write them like this:

function add(a: number, b: number) { return a + b; }
function sub(a: number, b: number) { return a - b; }
function mul(a: number, b: number) { return a * b; }
function div(a: number, b: number) { return a / b; }

or consolidate the repeated functions signatures with a single function type:

type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;

This has fewer type annotations than before, and they’re separated away from the function implementations. This makes the logic more apparent. You’ve also gained a check that the return type of all the function expressions is number. Libraries often provide types for common function signatures. For example, ReactJS provide a MouseEventHandler type that you can apply to an entire function rather than specifying MouseEvent as type for the function’s parameter. If you’re library author, consider providing type declarations for common callbacks.

2. Match the signature of some other function

In a web browser, for example, the fetch function issues an HTTP request for some resource:

const responseP = fetch('/quote?by=Mark+Twain');  // Type is Promise<Response>

You extract data from the response via response.json() or response.text():

async function getQuote() {
  const response = await fetch('/quote?by=Mark+Twain');
  const quote = await response.json();
  return quote;
}
// {
//   "quote": "If you tell the truth, you don't have to remember anything.",
//   "source": "notebook",
//   "date": "1894"
// }

There’s a bug here: if the request for /quote fails, the response body is likely to contain an explanation like 404 Not Found. This isn’t JSON, so response.json() will return a rejected Promise with a message about invalid JSON. This obscures the real error, which was a 404. It’s easy to forget that an error response with fetch not result in a rejected Promise. Let’s write a checkFetch function to do the status check for us. The type declarations for fetch in lib.dom.d.ts look like this:

declare function fetch(
  input: RequestInfo, init?: RequestInit
): Promise<Response>;

So you can write checkFetch like this:

async function checkedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (!response.ok) {
    // Converted to a rejected Promise in an async function
    throw new Error('Request failed: ' + response.status);
  }
  return response;
}

This works, but can be written more concisely:

const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error('Request failed: ' + response.status);
  }
  return response;
}

We’ve changed from a function statement to a function expression and applied a type typeof fetch to the entire function. This allows TS to inter the types of the input and init parameters.

The type annotation also guarantees that the return type of checkFetch will be the same as that of fetch. Had you written return instead of throw, for example, TS would have caught the mistake:

const checkedFetch: typeof fetch = async (input, init) => {
  //  ~~~~~~~~~~~~   Type 'Promise<Response | HTTPError>'
  //                     is not assignable to type 'Promise<Response>'
  //                   Type 'Response | HTTPError' is not assignable
  //                       to type 'Response'
  const response = await fetch(input, init);
  if (!response.ok) {
    return new Error('Request failed: ' + response.status);
  }
  return response;
}

The same mistake in the first example would likely have led to an error, but in the code that called checkFetch, rather than in the implementation.

In addition to being more concise, typing this entire function expression instead of its parameters has given you a better safety. When you’re writing a function that has the same type signature as another one, or writing many functions with the same type signature, consider whether you can apply a type declaration to entire functions, rather than repeating types of parameters and return values.

Things to remember

  • Consider applying type annotations to entire function expressions, rather than to their parameters and return type
  • If you’re writing the same type signature repeatedly, factor out a function type or look for an existing one. If you’re a library author, provide types for common callbacks.
  • Use typeof fn to match the signature of another function.
Published 17 Sep 2020

Read more

 — Angular Jira Clone Part 05 - Build an interactive drag and drop board
 — How to kill the process currently using a given port on Windows
 — Use VSCode Like a PRO
 — Angular Jira Clone Part 04 - Build an editable textbox
 — Angular Jira Clone Part 03 - Setup Akita state management

Follow @trungvose on Twitter for more!