search
HomeWeb Front-endJS TutorialHow to Use TypeScript to Accumulate Types: Typing ALL possible fetch() Results

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:

  1. Only care about the happy path: The response body when the HTTP status code is 2xx.

  2. 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;
</t>

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 newt>>;
    }
}

// Now you can use it like this:
const x = new NamePending();  // x is of type NamePending.
const y = x.accumulate  // y is of type NamePending.
</accumtype></newt></t>

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.
</tstatus></t></tstatus></t>

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>;
</tstatus></t></tstatus></statuscode>

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 tstatus tbody>>;
    }
}
</fetchresult></tstatus></t>

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;
</t>

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 newt>>;
    }
}

// Now you can use it like this:
const x = new NamePending();  // x is of type NamePending.
const y = x.accumulate  // y is of type NamePending.
</accumtype></newt></t>

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.
</tstatus></t></tstatus></t>

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>;
</tstatus></t></tstatus></statuscode>

“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 tstatus tbody>>;
    }
}
</fetchresult></tstatus></t>

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()
    .for()
    ;
/*
y's type:  DrFetch
*/

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!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Understanding the JavaScript Engine: Implementation DetailsUnderstanding the JavaScript Engine: Implementation DetailsApr 17, 2025 am 12:05 AM

Understanding how JavaScript engine works internally is important to developers because it helps write more efficient code and understand performance bottlenecks and optimization strategies. 1) The engine's workflow includes three stages: parsing, compiling and execution; 2) During the execution process, the engine will perform dynamic optimization, such as inline cache and hidden classes; 3) Best practices include avoiding global variables, optimizing loops, using const and lets, and avoiding excessive use of closures.

Python vs. JavaScript: The Learning Curve and Ease of UsePython vs. JavaScript: The Learning Curve and Ease of UseApr 16, 2025 am 12:12 AM

Python is more suitable for beginners, with a smooth learning curve and concise syntax; JavaScript is suitable for front-end development, with a steep learning curve and flexible syntax. 1. Python syntax is intuitive and suitable for data science and back-end development. 2. JavaScript is flexible and widely used in front-end and server-side programming.

Python vs. JavaScript: Community, Libraries, and ResourcesPython vs. JavaScript: Community, Libraries, and ResourcesApr 15, 2025 am 12:16 AM

Python and JavaScript have their own advantages and disadvantages in terms of community, libraries and resources. 1) The Python community is friendly and suitable for beginners, but the front-end development resources are not as rich as JavaScript. 2) Python is powerful in data science and machine learning libraries, while JavaScript is better in front-end development libraries and frameworks. 3) Both have rich learning resources, but Python is suitable for starting with official documents, while JavaScript is better with MDNWebDocs. The choice should be based on project needs and personal interests.

From C/C   to JavaScript: How It All WorksFrom C/C to JavaScript: How It All WorksApr 14, 2025 am 12:05 AM

The shift from C/C to JavaScript requires adapting to dynamic typing, garbage collection and asynchronous programming. 1) C/C is a statically typed language that requires manual memory management, while JavaScript is dynamically typed and garbage collection is automatically processed. 2) C/C needs to be compiled into machine code, while JavaScript is an interpreted language. 3) JavaScript introduces concepts such as closures, prototype chains and Promise, which enhances flexibility and asynchronous programming capabilities.

JavaScript Engines: Comparing ImplementationsJavaScript Engines: Comparing ImplementationsApr 13, 2025 am 12:05 AM

Different JavaScript engines have different effects when parsing and executing JavaScript code, because the implementation principles and optimization strategies of each engine differ. 1. Lexical analysis: convert source code into lexical unit. 2. Grammar analysis: Generate an abstract syntax tree. 3. Optimization and compilation: Generate machine code through the JIT compiler. 4. Execute: Run the machine code. V8 engine optimizes through instant compilation and hidden class, SpiderMonkey uses a type inference system, resulting in different performance performance on the same code.

Beyond the Browser: JavaScript in the Real WorldBeyond the Browser: JavaScript in the Real WorldApr 12, 2025 am 12:06 AM

JavaScript's applications in the real world include server-side programming, mobile application development and Internet of Things control: 1. Server-side programming is realized through Node.js, suitable for high concurrent request processing. 2. Mobile application development is carried out through ReactNative and supports cross-platform deployment. 3. Used for IoT device control through Johnny-Five library, suitable for hardware interaction.

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)Building a Multi-Tenant SaaS Application with Next.js (Backend Integration)Apr 11, 2025 am 08:23 AM

I built a functional multi-tenant SaaS application (an EdTech app) with your everyday tech tool and you can do the same. First, what’s a multi-tenant SaaS application? Multi-tenant SaaS applications let you serve multiple customers from a sing

How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration)How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration)Apr 11, 2025 am 08:22 AM

This article demonstrates frontend integration with a backend secured by Permit, building a functional EdTech SaaS application using Next.js. The frontend fetches user permissions to control UI visibility and ensures API requests adhere to role-base

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

AI Hentai Generator

AI Hentai Generator

Generate AI Hentai for free.

Hot Article

R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
1 months agoBy尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Best Graphic Settings
1 months agoBy尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. How to Fix Audio if You Can't Hear Anyone
1 months agoBy尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Chat Commands and How to Use Them
1 months agoBy尊渡假赌尊渡假赌尊渡假赌

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

Atom editor mac version download

Atom editor mac version download

The most popular open source editor

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Integrate Eclipse with SAP NetWeaver application server.

SecLists

SecLists

SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

VSCode Windows 64-bit Download

VSCode Windows 64-bit Download

A free and powerful IDE editor launched by Microsoft