Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 3985

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 61k
Author
Asked: November 26, 20242024-11-26T07:47:07+00:00 2024-11-26T07:47:07+00:00

Fetch with Typescript for better HTTP API Clients

  • 61k

Fetch is used in the browser to interact with API's over HTTP(S). Fetch works well for this. It is promise based. These are facts.

When you work with an API you often want type safety.

There is a popular piece by Kent C. Dodds on this. Kent wraps the request and handles coercing with some additional code. Check it out here.

I have my own preferred way to work with this using type intersections and pure functions.

My goal is to create a generic way to handle fetch; but I do not want to reshape the Response object, I want to pass that out.

This helps allow anyone consuming my fetch API client to extend for their own use cases.

If I make a design decision now to return something like { data: any, error: Error } I can paint myself into a corner.

Instead these clients will just return a full Response and when someone tries to use .json() the correct type will be set as if by magic (AKA inference, and narrowing*).

TL;DR

The code is here if you just want to get on with your day…

  /** For 201 */ type UserCreated = { id: string; name: string };  /** For 400 */ type BadRequest = { code: "bad_request"; message: string };  /** Response type intersection */ type UserResponse =   | (Omit<Response, "json"> & {       status: 201;       json: () => UserCreated | PromiseLike<UserCreated>;     })   | (Omit<Response, "json"> & {       status: 400;       json: () => BadRequest | PromiseLike<BadRequest>;     });  /** Marshalling stream to object with narrowing */ const marshalResponse = (res: UserResponse) => {   if (res.status === 201) return res.json();   if (res.status === 400) return res.json();   return Error("Unhandled response code"); };  /** Coerce Response to UserResponse */ const responseHandler = (response: Response) => {   const res = response as UserResponse;   return marshalResponse(res); };  /** Usage returns typed data */ const data = fetch(`https://api.com/v1/user`, {   method: "POST",   body: JSON.stringify({ name: "Simon" }), }).then((res) => responseHandler(res));   
Enter fullscreen mode Exit fullscreen mode

The type of data in the above is:

  UserCreated | BadRequest | Error;   
Enter fullscreen mode Exit fullscreen mode

I will deep dive this below and show how this all works.

An example

If you had an API that could create users you would send a POST request in fetch like below:

  fetch(`https://user.api.com`, {   method: "POST",   body: JSON.stringify({     name: "Simon",   }), });   
Enter fullscreen mode Exit fullscreen mode

This would create a promise, and that promise would have the type Promise<Response>.

To access the data that is returned from the API you would want to call .json() on the response.

The type of Response['json'] is a Promise<any> and that is not very helpful (this is a fact).

As part of the domain transfer layer you probably want to get your type back.

Lets suppose the API can return two bodies, one for a 201 code and one for a 400 code. Those will correspond to the Created and BadRequest responses.

  export type UserCreated = {   id: string;   name: string; };  export type BadRequest = {   code: "bad_request";   message: string; };   
Enter fullscreen mode Exit fullscreen mode

Our API will always return a 400 error of this kind, and a 201 on success.

Response Typing

We can use an intersection type on the Response type to narrow and restrict response to the two known types I expect.

Our user response look like this:

  /**  * The UserResponse is a Union of intersection that  * will narrow when the status is asserted  */ export type UserResponse =   | (Omit<Response, "json"> & {       status: 201;       json: () => UserCreated | PromiseLike<UserCreated>;     })   | (Omit<Response, "json"> & {       status: 400;       json: () => BadRequest | PromiseLike<BadRequest>;     });   
Enter fullscreen mode Exit fullscreen mode

I do a few things in this type:

  1. Omit (remove) the standard json that returns any. This is needed or I cannot narrow effectively because all types can be any and because that satisfies all constraints the compiler will helpfully always default to this.
  2. Make a union of intersections (&) that will narrow by relating the specific HTTP status code to a specific response.

I have used PromiseLike instead of promise, because that is the type that the underlying Response wants to have when it chains promises via then or catch.

Narrowing Response

Now I want to use the UserResponse type to handle the response from fetch. We want this type to narrow the possible values of .json() to BadRequest | UserCreated.

To do that I will perform assertions on the status:

  import { UserResponse } from "./response-user";  /**  * This is the Dto layer. If you want to coerce types like  * string representations of dates into real date objects  * then do it here.  */ export const marshalResponse = (res: UserResponse) => {   if (res.status === 201) return res.json();   if (res.status === 400) return res.json();   return Error("Unhandled response code"); };   
Enter fullscreen mode Exit fullscreen mode

I have returned an error here, but you don't need to, you could expand the interface to return an empty object {} if the response code is unhandled 👍.

Whatever you do, do not throw. Friends don't let friends use errors for flow control!

Next, I need to wrap the marshal function into a response handler.

  import { marshalResponse } from "./marshal"; import { UserResponse } from "./response-user";  export const responseHandler = (response: Response) => {   const res = response as UserResponse;    return marshalResponse(res); };   
Enter fullscreen mode Exit fullscreen mode

The responsibility of this function is to type cast the Response to a UserResponse and pass it into the marshal function.

Putting it all together

Let's first look at the await case.

  import { responseHandler } from "./handlers";  const response = await fetch(`https://api.com/v1/user`, {   method: "POST",   body: JSON.stringify({     name: "Simon",   }), });  export const body = await responseHandler(response);   
Enter fullscreen mode Exit fullscreen mode

So what type do you think body is now?

Fetch client response typed with await

And when we just use promises:

  import { responseHandler } from "./handlers";  export const data = fetch(`https://api.com/v1/user`, {   method: "POST",   body: JSON.stringify({     name: "Simon",   }), }).then((res) => responseHandler(res));   
Enter fullscreen mode Exit fullscreen mode

Fetch client response typed with await

Wrap up

This concludes the explanation of how to make fetch return the types that you want for your JSON bodies, without masking the underling Response type.

The possibilities for this kind of type are to create general API clients, that have inferred and narrowed response types, without writing lots of wrapper code, and without masking the underlying response object (open for extension).

I hope you find it useful.

javascriptprogrammingtypescriptwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 2k
  • Popular
  • Answers
  • Author

    ES6 - A beginners guide - Template Literals

    • 0 Answers
  • Author

    Understanding Higher Order Functions in JavaScript.

    • 0 Answers
  • Author

    Build a custom video chat app with Daily and Vue.js

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.