Using TypeScript with GraphQL Fetch: A Guide

I came across a similar question that almost solved my issue, but it didn't quite work for me because the endpoint I'm using is a graphQL endpoint with an additional nested property called query. For instance, if my query looks like this:

const query = `query Query($age: Int!){
    users(age: $age) {
      name
      birthday
    }
  }`;

Then according to the solution in the linked answer, the fetched object would be data.data.users, where the last property corresponds to the name of the graphql query itself. I tweaked the code from the link to this:

function graphQLFetch<T>(url: string, query: string, variables = {}): Promise<T> {
  return fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  }).then((response) => {
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    return response.json();
  })
    .then((responseJson) => responseJson.data[Object.keys(responseJson.data)[0]] as Promise<T>);
}

...and it works perfectly when sending a single query to the graphQL endpoint. But how can I make it more versatile so it can handle any number of queries? For example, what if I know my query will return User[] and Post[] as a tuple, based on the following graphql query:

const query = `query Query($age: Int!, $username: String!){
    users(age: $age) {
      name
      birthday
    }
    posts(username: $username) {
      date
    }
  }`;
}

In that case, I'd like something like this to function correctly:

const myData = graphQLFetch<[User[], Post[]]>(url, query, variables);

Do you think achieving this level of flexibility is feasible?

Answer №1

At the moment, it seems like your issue lies here...

responseJson.data[Object.keys(responseJson.data)[0]]

This code snippet will always retrieve only the first value from data.

Instead of using tuples for this scenario, I recommend returning the data object typed according to your expected response.

Let's start by defining a generic object type to represent GraphQL data

type GraphQlData = { [key: string]: any, [index: number]: never };

This represents the most basic form that the data can take. It essentially is a simple object with string keys. Adding never on numeric indexes prevents it from becoming an array.

Next, let's outline the structure of the GraphQL response

interface GraphQlResponse<T extends GraphQlData> {
  data: T;
  errors?: Array<{ message: string }>;
}

This interface defines the JSON response you receive from GraphQL, including the previous GraphQlData type or any specialization based on that. For instance, you could specify a particular response type as...

type UsersAndPostsResponse = GraphQlResponse<{ users: Users[], posts: Posts[] }>;

In this case,

{ users: Users[], posts: Posts[] }
is a more specific version of GraphQlData with keys limited to users and posts, along with specific value types.

Finally, create the function incorporating the same generic data type

async function graphQLFetch<T extends GraphQlData>(
  url: string,
  query: string,
  variables = {}
): Promise<T> {
  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) {
    throw new Error(`${res.status}: ${res.statusText}`);
  }

  // convert the response JSON to GraphQlResponse with the provided data type `T`
  const graphQlRes: GraphQlResponse<T> = await res.json();
  if (graphQlRes.errors) {
    throw new Error(graphQlRes.errors.map((err) => err.message).join("\n")); // consider creating a custom Error class
  }
  return graphQlRes.data;
}

You can then make your request in this manner

const { users, posts } = await graphQLFetch<{ users: User[]; posts: Post[] }>(
  url,
  query
);

Alternatively, if you need to retrieve all the values within data and return a tuple instead of a single record when there are multiple values present.

To support such a generic return type, you should define T as a union of singular or array types.

Note: There is a risk involved in assuming that the data values are ordered as specified. Using key-based access is generally safer.

// The generic type is now removed
interface GraphQlResponse {
  data: GraphQlData;
  errors?: Array<{ message: string }>;
}

async function graphQLFetch<T extends any | Array<any>>(
  url: string,
  query: string,
  variables = {}
): Promise<T> {
  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
  });
  if (!res.ok) {
    throw new Error(`${res.status}: ${res.statusText}`);
  }

  const graphQlRes: GraphQlResponse = await res.json();
  if (graphQlRes.errors) {
    throw new Error(graphQlRes.errors.map((err) => err.message).join("\n"));
  }

  const values = Object.values(graphQlRes.data);
  if (values.length === 1) {
    return values[0] as T;
  }
  return values as T;
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Option in TypeScript to retain the '//' in the filename when removed

I'm currently attempting to implement the angular2 quick start example in my local environment. The only feasible way for me to serve the application is as a plugin within an existing application on my system. This particular application hosts a web ...

Mistakes following update to Angular 4 from Angular 2

After upgrading from Angular2 to Angular4, I encountered these errors in the CLI. While my app continues to function after the upgrade, I am curious about possible solutions to resolve these errors. Any suggestions? https://i.stack.imgur.com/CyYqw.png He ...

The utilization of rxjs' isStopped function is now considered

We currently have this method implemented in our codebase: private createChart(dataset: any): any { if (!this.unsubscribeAll.isStopped) { this.chart = this.miStockChartService.createChart(dataset, this.chartId, this.options, this.extend ...

Angular2 routing directs me to a different URL path

In my Angular2 web application, I have set up the following routes: '', '/app', '/login', '/signin', '/validate', '/error', '**': I've defined a route configuration in app.router.t ...

Are there any methods to utilize Zod for validating that a number contains a maximum of two decimal places?

How can I ensure that a numeric property in my object has only up to 2 decimal digits? For example: 1 // acceptable 1.1 // acceptable 1.11 // acceptable 1.111 // not acceptable Is there a method to achieve this? I checked Zod's documentation and sea ...

What is the best way to incorporate interface validation in TypeScript switch statements?

Currently utilizing Typescript and redux My goal is to compare a passed action with an interface, then execute the appropriate task export function counterReducer( state = initialState, action: CounterActionTypes ): CounterState { switch (action) { ...

Using Express middleware in a TypeScript Express application

I'm currently converting the backend of an ExpressJS application to Typescript. While working on the auth.routes.ts file, I encountered an issue with the middleware (authMiddleware). It seems like there might be a typing error, as the same code in the ...

Tips for managing Razorpay responses in Angular 2

I'm currently in the process of finalizing my payment transaction through RazorPay Payment gateway, and I've attempted to do so as shown below: var options = { "key": "XXX", "amount": 100, "name": "Ezshipp", "description": this.it ...

Is there any way to deactivate the saved query in react-admin without having to develop a new component?

The latest version of react-admin, version 4, introduced a new feature that allows saving filters. I'm curious about how to disable this functionality without having to create an additional filter button. https://i.stack.imgur.com/uTrUe.gif ...

What steps should I take to ensure that this test covers all possible scenarios?

I recently started learning React development and am currently exploring testing with Jest and React Testing Library (RTL). However, I'm facing challenges in achieving complete test coverage for the component code below: import { CustomCardActions, ...

Unable to locate a type definition file for module 'vue-xxx'

I keep encountering an error whenever I attempt to add a 3rd party Vue.js library to my project: Could not find a declaration file for module 'vue-xxx' Libraries like 'vue-treeselect', 'vue-select', and 'vue-multiselect ...

Compiling the configureStore method with the rootreducer results in compilation failure

Software Setup @angular/cli version: ^9.1.2 System Details NodeJS Version: 14.15.1 Typescript Version: 4.0.3 Angular Version: 10.1.6 @angular-redux/store version: ^11.0.0 @angular/cli version (if applicable): 10.1.5 OS: Windows 10 Expected Outcome: ...

The pathway specified is untraceable by the gulp system

Hey there, I've encountered an issue with my project that uses gulp. The gulpfile.js suddenly stopped working without any changes made to it. The output I'm getting is: cmd.exe /c gulp --tasks-simple The system cannot find the path specified. ...

What is the best method for retrieving an item from localstorage?

Seeking advice on how to retrieve an item from local storage in next.js without causing a page rerender. Here is the code snippet I am currently using: import { ThemeProvider } from "@material-ui/core"; import { FC, useEffect, useState } from "react"; i ...

What is the best way to define the type of an object when the Key is already known?

If I have the code snippet below, how can I properly define the data object type based on the known value of data.type? In this scenario, I aim to assign a specific type to data when its type property is either "sms" or "email" const payload = '{&quo ...

When renaming a Vuex module, the system mistakenly believes the old module is still present

After attempting to rename my modules, I encountered a perplexing error. Originally, I had a module named "speed" which I decided to change to "team". Despite updating all references accordingly, I am now facing the following error message: Error: Missing ...

Please explain the purpose of the httponly ss-tok bearerToken cookie in ServiceStack Authentication

While I comprehend the importance of implementing an httponly flag in the Set-Cookie Response header to enhance security and prevent XSS attacks, there is one aspect that remains unclear to me. Specifically, I am unsure about the purpose of the "ss-tok" co ...

The RC-dock library's 'DockLayout' is not compatible with JSX components. The instance type 'DockLayout' is not a valid JSX element and cannot be used as such

Despite encountering similar questions, none of the provided answers seem to address the issue within my codebase. My project utilizes React 17, Mui v5, and TS v4. I attempted to integrate a basic component from an external package called rc-dock. I simply ...

TypeScript recursive object mapping utility

I am currently developing a type-safe object mapper in TypeScript. So far, I have successfully implemented this at a single level of depth using this playground. enum TransformerActions { Delete = 'delete', } type TransformerMap<S> = ...

We regret to inform you that an application error has occurred: a client-side exception has been encountered. Please refer to the browser

Fix for Nextjs Blog App Error If you encounter the following error message: Application error: a client-side exception has occurred (see the browser console for more information). when trying to create a post on AWS Amplify console and DynamoDB is failing ...