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.
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.
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.
typeof fn
to match the signature of another function.