Kokodoko
Kokodoko

Reputation: 28128

How to use fetch in TypeScript

I am using window.fetch in Typescript, but I cannot cast the response directly to my custom type:

I am hacking my way around this by casting the Promise result to an intermediate 'any' variable.

What would be the correct method to do this?

import { Actor } from './models/actor';

fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json())
      .then(res => {
          // this is not allowed
          // let a:Actor = <Actor>res;

          // I use an intermediate variable a to get around this...
          let a:any = res; 
          let b:Actor = <Actor>a;
      })

Upvotes: 183

Views: 417958

Answers (6)

Jos&#233; Ram&#237;rez
Jos&#233; Ram&#237;rez

Reputation: 2315

Newer Solution Available (2024-12)

I got interested in this topic when I was faced with the problem myself at work. The most accepted answer is a big NO to me because:

  1. It only types the happy path: When response.ok is true
  2. Throws errors on non-ok responses, which promotes the "follow the happy path" code smell

Also, why do an if (response.ok) to throw an error? This forces you to add a try..catch somewhere. Just test for the status code or the ok value. Throwing is a terrible, terrible practice.

Anyway, I needed to type all possible bodies: For example, my API returns a meaningful body on 400 BAD REQUEST responses.

There was nothing out there that solves the N-types-for-N-status-codes problem, so I made my own: The NPM package dr-fetch resolves this by accumulating types using chain syntax.

First, create the fetcher object, which allows for custom body parsers and customized data-fetching functions. The former is to handle responses with content-type's that are uncommon; the latter is to do pre/post fetch work:

// myFetch.ts
import { obtainToken } from './magical-auth-stuff.js';
import { setHeaders } from 'dr-fetch';

export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
    const token = obtainToken();
    init ??= {};
    setHeaders(init, { 'Accept': 'application/json', 'Authorization': `Bearer ${token}`});
    return fetch(url, init);
}

// fetcher.ts
import { DrFetch } from "dr-fetch";
import { myFetch } from "./myFetch.js";

export default new DrFetch(myFetch);
// If you don't need a custom fetch function, just do:
export default new DrFetch();

Now we import this fetcher object where needed, type it for the URL, and fetch:

import fetcher from './fetcher.js';
import type { ServerErrorStatusCode } from 'dr-fetch';

const response = await fetcher
    .for<200, MySuccessType>()
    .for<400, MyBadRequestType>()
    .for<500, MyServerErrorType>()
    // OR, with a helper type to cover more status codes at once:
    .for<ServerErrorStatusCode, MyServerErrorType>()
    .post('the/url', { my: body })
    ;

// At this point, TypeScript can type-narrow 'response.body' using 
// 'response.status' or 'response.ok':
if (response.status === 501) {
    // response.body is of type MyServerErrorType
}
else if (response.status === 200) {
    // response.body is of type MySuccessType
}

See? This is very clear, concise code that doesn't require try..catch for non-OK responses. Stop the madness: Say no to throwing on non-OK responses.

Upvotes: 0

Chris
Chris

Reputation: 58142

Await version

Unfortunately, neither fetch, nor the response it returns accept generic input, so we need to do some type casting (seen as as T below).

However, it is common to wrap fetch in your own project to provide any common headers, a base url, etc, and so you can allow generic input within your application.

Example of wrapping fetch, and allowing the application to define the shape of each api response.

const BASE_URL = 'http://example.org';

//         Input T ↴   is thread through to ↴
async function api<T>(path: string): Promise<T> {
    const response = await fetch(`${BASE_URL}/${path}`);

    if (!response.ok) {
      throw new Error(response.statusText);
    }

    //    And can also be used here ↴
    return await response.json() as T;
}

// Set up various fetches
async function getConfig() {
  //             Passed to T ↴
  return await api<{ version: number }>('config');
}

// Elsewhere
async function main() {
  const config = await getConfig();

  // At this point we can confidently say config has a .version
  // of type number because we threaded the shape of config into 
  // api() 
  console.log(config.version); 
}

Playground

Previous versions

Note: The previous versions used promises at the time of writing, prior to await/async being commonly found in the browser. They are still useful from a learning perspective, and so they are kept.

Promise version

There has been some changes since writing this answer a while ago. As mentioned in the comments, response.json<T> is no longer valid. Not sure, couldn't find where it was removed.

For later releases, you can do:

// Standard variation
function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json() as Promise<T>
    })
}


// For the "unwrapping" variation

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json() as Promise<{ data: T }>
    })
    .then(data => {
        return data.data
    })
}

Old Answer

A few examples follow, going from basic through to adding transformations after the request and/or error handling:

Basic:

// Implementation code where T is the returned data shape
function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<T>()
    })

}

// Consumer
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

Data transformations:

Often you may need to do some tweaks to the data before its passed to the consumer, for example, unwrapping a top level data attribute. This is straight forward:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<{ data: T }>()
    })
    .then(data => { /* <-- data inferred as { data: T }*/
      return data.data
    })
}

// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

Error handling:

I'd argue that you shouldn't be directly error catching directly within this service, instead, just allowing it to bubble, but if you need to, you can do the following:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<{ data: T }>()
    })
    .then(data => {
      return data.data
    })
    .catch((error: Error) => {
      externalErrorLogging.error(error) /* <-- made up logging service */
      throw error /* <-- rethrow the error so consumer can still catch it */
    })
}

// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

Upvotes: 302

koalaok
koalaok

Reputation: 5720

For this particular use-case:

"Fetching data from a remote resource, we do not have control and want to validate filter before injecting in our current application"

I feel recommending zod npm package https://www.npmjs.com/package/zod

with the following fashion:

// 1. Define a schema

const Data = z.object({
  // subset of real full type
  name: z.string(),
  // unExpectedAttr: z.number(), --> enabling this will throw ZodError 
  height: z.string(),
  mass: z.string(),
  films: z.array(z.string()),
});

// 2. Infer a type from the schema to annotate the final obj

type DataType = z.infer<typeof Data>;

(async () => {
  try {
    const r = await fetch(`https://swapi.dev/api/people/1/?format=json`);
    const obj: DataType = Data.parse(await r.json());
    console.log(obj); // filtered with expected field in Data Schema
    /**
     Will log:
     {
       name: 'Luke Skywalker',
       height: '172',
       mass: '77',
       films: [
        'https://swapi.dev/api/films/1/',
        'https://swapi.dev/api/films/2/',
        'https://swapi.dev/api/films/3/',
        'https://swapi.dev/api/films/6/'
       ]
     }
    */

  } catch (error) {
    if (error instanceof ZodError) {
      // Unexpected type in response not matching Data Schema
    } else {
      // general unexpected error
    }
  }
})();

Upvotes: 2

Yilmaz
Yilmaz

Reputation: 49182

This is specifically written for POST request. That is why it has "variables" parameter. In case of "GET" request same code will work, vriables can be optional is handled

export type FetcherOptions = {
  queryString: string
  variables?: FetcherVariables
}

export type FetcherVariables = {[key: string]: string | any | undefined}

export type FetcherResults<T> = {
  data: T
}

const fetcher = async <T>({queryString, 
                           variables }: FetcherOptions): Promise<FetcherResults<T>> => {
  const res = await fetch(API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // You can add more headers
    },
    body: JSON.stringify({
      queryString,
      variables
    })
  })
  const { data, errors} = await res.json()

  if (errors) {
    // if errors.message null or undefined returns the custom error
    throw new Error(errors.message ?? "Custom Error" )
  }

  return { data }
}

Upvotes: 3

Rodney P. Barbati
Rodney P. Barbati

Reputation: 2090

Actually, pretty much anywhere in typescript, passing a value to a function with a specified type will work as desired as long as the type being passed is compatible.

That being said, the following works...

 fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json())
      .then((res: Actor) => {
          // res is now an Actor
      });

I wanted to wrap all of my http calls in a reusable class - which means I needed some way for the client to process the response in its desired form. To support this, I accept a callback lambda as a parameter to my wrapper method. The lambda declaration accepts an any type as shown here...

callBack: (response: any) => void

But in use the caller can pass a lambda that specifies the desired return type. I modified my code from above like this...

fetch(`http://swapi.co/api/people/1/`)
  .then(res => res.json())
  .then(res => {
      if (callback) {
        callback(res);    // Client receives the response as desired type.  
      }
  });

So that a client can call it with a callback like...

(response: IApigeeResponse) => {
    // Process response as an IApigeeResponse
}

Upvotes: 17

nicowernli
nicowernli

Reputation: 3348

If you take a look at @types/node-fetch you will see the body definition

export class Body {
    bodyUsed: boolean;
    body: NodeJS.ReadableStream;
    json(): Promise<any>;
    json<T>(): Promise<T>;
    text(): Promise<string>;
    buffer(): Promise<Buffer>;
}

That means that you could use generics in order to achieve what you want. I didn't test this code, but it would looks something like this:

import { Actor } from './models/actor';

fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json<Actor>())
      .then(res => {
          let b:Actor = res;
      });

Upvotes: 5

Related Questions