Search code examples
typescriptpromisefetch-api

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

Solution

  • 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 */
      })