Typing a fetch request with TypeScript

Migrating vanilla JavaScript to TypeScript usually surfaces a handful of type errors right away. But even after those initial complaints are resolved, you may find that fetch calls return types that are looser than you'd like. Here's a walkthrough of the additional typing work required when the response shape matters.

The motivating example is a helper that queries the GraphQL Pokemon API:

async function fetchPokemon(name: string) {
  const response = await fetch(`https://graphql-pokemon.now.sh/?query=${query}`)
  const pokemon = await response.json()
  return pokemon
}

Once the filename is updated to .ts, the compiler flags the missing types on each function parameter. Adding : string to the parameter of fetchPokemon (and to any other implicit any parameters in helper functions) clears those errors. But the return type is Promise<any>, which means callers get no help from the compiler when they access properties on the resolved value.

Declaring the response shape

Suppose the API returns data like this:

{
  "data": {
    "pokemon": {
      "name": "pikachu",
      "attacks": {
        "special": [
          { "name": "Thunderbolt", "type": "Electric", "damage": 55 }
        ]
      }
    }
  }
}

In that case, writing pikachu.attacks.special.name produces no compile-time error, because the value is any. The fix is to describe the expected payload explicitly:

type PokemonData = {
  name: string
  attacks: {
    special: { name: string; type: string; damage: number }[]
  }
}

Then annotate the return value of fetchPokemon as Promise<PokemonData>. Now incorrect property access is caught at compile time, and the caller can safely rely on the shape of the data.

Handling the JSON response

The underlying issue is that response.json() returns Promise<any>. TypeScript has no way to infer the response body's structure from a fetch call, so the compiler leaves it as any.

To fix this without losing type safety, give the JSON body an explicit type annotation. For a request that returns { data: PokemonData }, this looks like:

const pokemon = (await response.json()) as { data: PokemonData }

Once the response is typed, any subsequent use of errors.map or similar array methods no longer needs manual type annotations on the callback parameters.

Monkey-patching with types

A common pattern is to augment an object returned from an API with additional client-side properties. For example:

pokemon.fetchedAt = new Date()

If the declared type is PokemonData, the compiler complains that fetchedAt does not exist. And if you add it to the type, it then complains that the function is missing the required property at the return point. (One option is to mark it optional, using Omit<PokemonData, 'fetchedAt'> for the API response, or to use a type assertion.)

A cleaner solution is to use Object.assign, which TypeScript understands as merging properties onto the target object. Combining the response data with the extra field in one expression satisfies the compiler's expectations for both input and output:

return Object.assign(pokemon, { fetchedAt: new Date() })

The compiler's type definition for Object.assign declares the return value as an intersection of the target and source objects, so the resulting value is fully typed.

Typing rejected promises

The Promise generic only captures the resolved value. There is no parameter for the type of a rejection reason — you cannot write Promise<PokemonData, SomeErrorType>. The rationale is that errors can be thrown for unforeseen reasons, so TypeScript makes no claim about what an unhandled rejection might be. It's a limitation worth knowing about, even if it means the error branch of a fetch call stays loosely typed.

Final structure

The finished helper includes:

  1. Explicit parameter and return types on the exported function.
  2. A typed response body, cast at the point of JSON parsing.
  3. An Omit type to separate the raw API shape from the augmented client object.
  4. Use of Object.assign instead of direct property mutation to keep the compiler aware of the final shape.

The result is a fully typed fetch wrapper where callers get meaningful compile-time feedback, and the code no longer relies on implicit any values.