155

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;
      })
VLAZ
  • 26,331
  • 9
  • 49
  • 67
Kokodoko
  • 26,167
  • 33
  • 120
  • 197
  • Uh, `json` contains plain objects, so how could you cast it to an instance? You'd need to use something like `Actor.from` that creates a `new Actor` with the data. – Bergi Dec 12 '16 at 14:45
  • Why is it "not allowed"? What error do you get when you try it? – Bergi Dec 12 '16 at 14:46
  • 1
    and which definitions are you using because [fetch isn't in typescript libs yet](https://github.com/Microsoft/TypeScript/pull/12493) – Meirion Hughes Dec 12 '16 at 14:47
  • Ah, I'm sorry, I just discovered the error: I have to say that res is of type any. .then((res:any) => { let b = res}). Then it's actually allowed. @MeirionHughes I am using the definitelyTyped whatwg-fetch.d.ts files to make typescript recognise fetch. – Kokodoko Dec 12 '16 at 14:50
  • @Bergi [fetch is now implemented in ts](https://github.com/microsoft/TypeScript/pull/13856) – Timo Feb 15 '22 at 16:06
  • 1
    @Timo Was this comment supposed to be directed at Meirion? – Bergi Feb 15 '22 at 16:27
  • @MeirionHughes fetch is now implemented in ts. – Timo Feb 16 '22 at 08:10

5 Answers5

253

Edit

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 */
  })
Spikatrix
  • 20,225
  • 7
  • 37
  • 83
Chris
  • 54,599
  • 30
  • 149
  • 186
  • 2
    Thanks, that's the best explanation of generics I've read so far. It's still a bit vague why a Promise can be of a type, while it's actually the data that has the type... – Kokodoko Mar 25 '18 at 10:45
  • Great! I've been exploring this part of TS more recently, so its helpful for me to jot down my notes. Which part is confusing? - happy to expand on it – Chris Mar 25 '18 at 10:49
  • I'd expect that it's not the Promise that has the `` type, but the content that is being fetched. But apparently you can tell that to the Promise class? (Is a Promise a class? a function? an object?) – Kokodoko Mar 25 '18 at 15:12
  • What if I have the errors in the response body rather than in the status text (and I can't control that) – paqash Mar 18 '19 at 13:35
  • Just replace `if (!response.ok) {` with whatever check you need – Chris Mar 19 '19 at 00:33
  • 10
    The response.json method does not seem to be defined as generic -- neither in the current `@types/node-fetch`, nor in the current TypeScript `lib.dom.d.ts` -- so this answer isn't feasible now. So instead I guess we have to do `return response.json() as Promise;`? – ChrisW May 03 '19 at 09:33
  • 1
    @ChrisW You're correct it has changed. I've updated the answer – Chris May 03 '19 at 11:00
  • For those interested you can add an optional parameter `unwrapParam?: string` to make the unwrap more generic: `return (response.json() as Promise<{ [wrapParam: string]: T }>).then((data) => data[unwrapParam]` – Alan Yeung Jul 09 '21 at 03:25
  • how can promise.json() be a Promise? – Rahul Yadav Sep 27 '21 at 20:16
  • @RahulYadav https://developer.mozilla.org/en-US/docs/Web/API/Response/json#return_value – Chris Sep 28 '21 at 00:02
12

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
}
Rodney P. Barbati
  • 1,883
  • 24
  • 18
6

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;
      });
Jason Aller
  • 3,541
  • 28
  • 38
  • 38
nicowernli
  • 3,250
  • 22
  • 37
  • 20
    Adding the generic type results in `Expected 0 type arguments, but got 1`, but perhaps that's because I don't use `node-fetch`. The types for native fetch are probably different? – Kokodoko Oct 18 '17 at 15:53
  • 19
    the linked file does not have a templated `json()`. if it existed before, it doesn't anymore. – ryanrhee May 31 '18 at 06:42
  • 4
    Sorry to dredge up an old post, but, the `Body.json` signature was removed from DefinitelyTyped in 2018 with [this commit](https://github.com/DefinitelyTyped/DefinitelyTyped/commit/78cdbac#diff-16dd1699972b26236bcd5bc367af23fbL128). Browsing through the history of [**node-fetch** > `/src/body.js`](https://github.com/node-fetch/node-fetch/blob/master/src/body.js), I'm not sure I ever see a time where `json()` was implemented this way. Nico, do you have an example of this working, otherwise, I'm inclined to think this doesn't. – KyleMit Sep 29 '20 at 01:15
  • Well I took that from some code I had in 2017... I don't know the state of this right now – nicowernli Oct 05 '20 at 09:02
2

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

koalaok
  • 5,075
  • 11
  • 47
  • 91
1

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 }
}
Yilmaz
  • 35,338
  • 10
  • 157
  • 202