


How to Use TypeScript to Accumulate Types: Typing ALL possible fetch() Results
When I started re-writing (with my team) our application in TypeScript and Svelte (it was in JavaScript and React which we all hate), I was faced with a problem:
How can I safely type all possible bodies of an HTTP response?
Does this ring a bell to you? If not, you’re most likely “one of those”, hehe. Let’s digress for a moment to understand the picture better.
Why This Area Seems Unexplored
Nobody seems to care about “all possible bodies” of an HTTP response, as I could not find anything out there already made for this (well, maybe ts-fetch). Let me quickly go through my logic here about why this is.
Nobody cares because people either:
Only care about the happy path: The response body when the HTTP status code is 2xx.
People manually type it elsewhere.
For #1, I’d say that yes, developers (especially the inexperienced ones) forget that an HTTP request may fail, and that the information carried in the failed response is most likely completely different to the regular response.
For #2, let’s dig into a big issue found in popular NPM packages like ky and axios.
The Problem With Data-Fetching Packages
As far as I can tell, people like packages like ky or axios because one of their “features” is that they throw an error on non-OK HTTP status codes. Since when is this OK? Since never. But apparently people are not picking this up. People are happy and content getting errors on non-OK responses.
I imagine that people type non-OK bodies when it is time to catch. What a mess, what a code smell!
This is a code smell because you are effectively using try..catch blocks as branching statements, and try..catch is not meant to be a branching statement.
But even if you were to argue with me that branching happens naturally in try..catch, there is another big reason why this remains bad: When an error is thrown, the runtime needs to unwind the call stack. This is far more costly in terms of CPU cycles than regular branching with an if or switch statement.
Knowing this, can you justify the performance hit just to misuse the try..catch block? I say no. I cannot think of a single reason why the front-end world seems to be perfectly happy with this.
Now that I have explained my line of reasoning, let’s get back to the main topic.
The Problem, Detailed
An HTTP response may carry different information depending on its status code. For example, a todo endpoint such as api/todos/:id that receives a PATCH HTTP request may return a response with a different body when the response’s status code is 200 than when the response’s status code is 400.
Let’s exemplify:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
So, with this in mind we go back to the problem statement: How can I type a function that does this PATCH request where TypeScript can tell me which body I’m dealing with, depending on the HTTP status code as I write code? The answer: Use fluent syntax (builder syntax, chained syntax) to accumulate types.
Building the Solution
Let’s start by defining a type that builds upon a previous type:
export type AccumType<T, NewT> = T | NewT;
Super simple: Given types T and NewT, join them to form a new type. Use this new type as T again in AccumType<>, and then you can accumulate another new type. This done by hand, however, is not nice. Let’s introduce another key piece for the solution: Fluent syntax.
Fluent Syntax
Given an object of class X whose methods always return itself (or a copy of itself), one can chain method calls one after the other. This is fluent syntax, or chained syntax.
Let’s write a simple class that does this:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
Eureka! We have successfully combined fluent syntax and the type we wrote to start accumulating data types into a single type!
In case it is not evident, you can continue the exercise until you’ve accumulated the desired types (x.accumulate().accumulate()… until you’re done).
This is all good and nice, but this super simple type is not tying up the HTTP status code to the corresponding body type.
Refining What We Have
What we want is to provide TypeScript with enough information so that its type-narrowing feature kicks in. To do this, let’s do the needful to obtain code that is relevant to the original problem (typing bodies of HTTP responses in a per-status code basis).
First, rename and evolve AccumType. The code below shows the progression in iterations:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
At this point, I realized something: Status codes are finite: I can (and did) look them up and define types for them, and use those types to restrict type parameter TStatus:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
We have arrived to a series of types that are just beautiful: By branching (writing if statements) based on conditions on the ok or the status property, TypeScript’s type-narrowing function will kick in! If you don’t believe it, let’s write the class part and try it out:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Test-drive this:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
It should now be clear why type-narrowing will be able to correctly predict the shape of the body when branching, based on the ok property of the status property.
There’s an issue, however: The initial typing of the class when it is instantiated, marked in the comment block above. I solved it like this:
export type AccumType<T, NewT> = T | NewT;
This small change effectively excludes the initial typing, and we are now in business!
Now we can write code like the following, and Intellisense will be 100% accurate:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
Type-narrowing will also work when querying for the ok property.
If you did not notice, we were able to write much better code by not throwing errors. In my professional experience, axios is wrong, ky is wrong, and any other fetch helper out there doing the same is wrong.
Conclusion
TypeScript is fun, indeed. By combining TypeScript and fluent syntax, we are able to accumulate as many types as needed so we can write more accurate and clearer code from day 1, not after debugging over and over. This technique has proven successful and is live for anyone to try. Install dr-fetch and test-drive it:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
A More Complex Package
I also created wj-config, a package that aims towards the complete elimination of the obsolete .env files and dotenv. This package also uses the TypeScript trick taught here, but it joins types with &, not |. If you want to try it out, install v3.0.0-beta.1. The typings are much more complex, though. Making dr-fetch after wj-config was a walk in the park.
Fun Stuff: What’s Out There
Let’s see a few of the errors out there in fetch-related packages.
isomorphic-fetch
You can see in the README this:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
“Bad response from server”?? Nope. “Server says your request is bad”. Yes, the throwing part itself is terrible.
ts-fetch
This one has the right idea, but unfortunately can only type OK vs non-OK responses (2 types maximum).
ky
One of the packages that I criticized the most, shows this example:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
This is what a very junior developer would write: Just the happy path. The equivalence, according to its README:
const x = new DrFetch<{}>(); // Ok, having to write an empty type is inconvenient. const y = x .for<200, { a: string; }>() .for<400, { errors: string[]; }>() ; /* y's type: DrFetch<{ ok: true; status: 200; statusText: string; body: { a: string; }; } | { ok: false; status: 400; statusText: string; body: { errors: string[]; }; } | {} // <-------- WHAT IS THIS!!!??? > */
The throwing part is so bad: Why would you branch to throw, to force you to catch later? It makes zero sense to me. The text in the error is misleading, too: It is not a “fetch error”. The fetching worked. You got a response, didn’t you? You just didn’t like it… because it is not the happy path. Better wording would be “HTTP request failed:”. What failed was the request itself, not the fetching operation.
The above is the detailed content of How to Use TypeScript to Accumulate Types: Typing ALL possible fetch() Results. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

AI Hentai Generator
Generate AI Hentai for free.

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics



Detailed explanation of JavaScript string replacement method and FAQ This article will explore two ways to replace string characters in JavaScript: internal JavaScript code and internal HTML for web pages. Replace string inside JavaScript code The most direct way is to use the replace() method: str = str.replace("find","replace"); This method replaces only the first match. To replace all matches, use a regular expression and add the global flag g: str = str.replace(/fi

So here you are, ready to learn all about this thing called AJAX. But, what exactly is it? The term AJAX refers to a loose grouping of technologies that are used to create dynamic, interactive web content. The term AJAX, originally coined by Jesse J

Article discusses creating, publishing, and maintaining JavaScript libraries, focusing on planning, development, testing, documentation, and promotion strategies.

The article discusses strategies for optimizing JavaScript performance in browsers, focusing on reducing execution time and minimizing impact on page load speed.

The article discusses effective JavaScript debugging using browser developer tools, focusing on setting breakpoints, using the console, and analyzing performance.

Bring matrix movie effects to your page! This is a cool jQuery plugin based on the famous movie "The Matrix". The plugin simulates the classic green character effects in the movie, and just select a picture and the plugin will convert it into a matrix-style picture filled with numeric characters. Come and try it, it's very interesting! How it works The plugin loads the image onto the canvas and reads the pixel and color values: data = ctx.getImageData(x, y, settings.grainSize, settings.grainSize).data The plugin cleverly reads the rectangular area of the picture and uses jQuery to calculate the average color of each area. Then, use

This article will guide you to create a simple picture carousel using the jQuery library. We will use the bxSlider library, which is built on jQuery and provides many configuration options to set up the carousel. Nowadays, picture carousel has become a must-have feature on the website - one picture is better than a thousand words! After deciding to use the picture carousel, the next question is how to create it. First, you need to collect high-quality, high-resolution pictures. Next, you need to create a picture carousel using HTML and some JavaScript code. There are many libraries on the web that can help you create carousels in different ways. We will use the open source bxSlider library. The bxSlider library supports responsive design, so the carousel built with this library can be adapted to any

Data sets are extremely essential in building API models and various business processes. This is why importing and exporting CSV is an often-needed functionality.In this tutorial, you will learn how to download and import a CSV file within an Angular
