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

Adjust the component suppliers based on the @input

If I were to implement a material datepicker with a selection strategy, I would refer to this example There are instances where the selection strategy should not be used. The challenge lies in setting the selection strategy conditionally when it is insta ...

Deleting specialized object using useEffect hook

There's a simple vanilla JS component that should be triggered when an element is added to the DOM (componentDidMount) and destroyed when removed. Here's an example of such a component: class TestComponent { interval?: number; constructor() ...

Getting Session from Next-Auth in API Route: A Step-by-Step Guide

When printing my session from Next Auth in a component like this, I can easily see all of its data. const session = useSession(); // ... <p>{JSON.stringify(session)}</p> I am facing an issue where I need to access the content of the session i ...

Utilize Sequelize's cascade feature within your application

I'm currently building a web application using sequelize and typescript. In my database, I have three tables: WfProjectObject, which is connected to WfProject, and WfStep, also linked to WfProject. My goal is to include WfStep when querying WfProject ...

The address :::3000 is already in use by NestJS

While attempting to deploy my NestJs server on a C-Panel hosting, I have encountered an issue. Despite properly installing all node_modules and ensuring every project file is in place, the server fails to start and continuously displays the following error ...

There are critical vulnerabilities in preact-cli, and trying to run npm audit fix leads to a never-ending loop between versions 3.0.5 and 2.2.1

Currently in the process of setting up a preact project using preact-cli: npx --version # 7.4.0 npx preact-cli create typescript frontend Upon completion, the following information is provided: ... added 1947 packages, and audited 1948 packages in 31s 12 ...

Creating a welcome screen that is displayed only the first time the app is opened

Within my application, I envisioned a startup/welcome page that users would encounter when the app is launched. They could then proceed by clicking a button to navigate to the login screen and continue using the app. Subsequently, each time the user revisi ...

Transforming JavaScript code with Liquid inline(s) in Shopify to make it less readable and harder to understand

Today, I discovered that reducing JavaScript in the js.liquid file can be quite challenging. I'm using gulp and typescript for my project: This is a function call from my main TypeScript file that includes inline liquid code: ajaxLoader("{{ &ap ...

Could you explain the distinction between npm install and sudo npm install?

I recently switched to using linux. To install typescript, I ran the following command: npm i typescript Although there were no errors during the installation process, when I checked the version by typing tsc --version, I encountered the error message -bas ...

Developing J2EE servlets with Angular for HTTP POST requests

I've exhausted my search on Google and tried numerous PHP workarounds to no avail. My issue lies in attempting to send POST parameters to a j2ee servlet, as the parameters are not being received at the servlet. Strangely though, I can successfully rec ...

Is there a possibility in TypeScript to indicate "as well as any additional ones"?

When working with typescript, it's important to define the variable type. Consider this example: userData: {id: number, name: string}; But what if you want to allow other keys with any types that won't be validated? Is there a way to achieve thi ...

Managing a MySQL database in NodeJS using Typescript through a DatabaseController

I'm currently working on a restAPI using nodejs, express, and mysql. My starting point is the app.js file. Within the app.js, I set up the UserController: const router: express.Router = express.Router(); new UserController(router); The UserControll ...

Discovering the category for ethereum, provider, and contract

My current interface looks like this: interface IWeb3 { ethereum?: MetaMaskInpageProvider; provider?: any; contract?: any; }; I was able to locate the type for ethereum using import { MetaMaskInpageProvider } from "@metamask/providers", ...

Error TS[2339]: Property does not exist on type '() => Promise<(Document<unknown, {}, IUser> & Omit<IUser & { _id: ObjectId; }, never>) | null>'

After defining the user schema, I have encountered an issue with TypeScript. The error message Property 'comparePassword' does not exist on type '() => Promise<(Document<unknown, {}, IUser> & Omit<IUser & { _id: Object ...

nodemon failing to automatically refresh files in typescript projects

I am currently developing an app using NodeJs, express, typescript, and nodemon. However, I am encountering an issue where the page does not refresh when I make changes to the ts files. Is there a way for me to automatically compile the ts files to js an ...

"Uh-oh! Debug Failure: The statement is incorrect - there was a problem generating the output" encountered while attempting to Import a Custom Declarations File in an Angular

I am struggling with incorporating an old JavaScript file into my Angular service. Despite creating a declaration file named oldstuff.d.ts, I am unable to successfully include the necessary code. The import statement in my Angular service seems to be worki ...

How come TypeScript does not detect when a constant is used prior to being assigned?

There's an interesting scenario I came across where TypeScript (3.5.1) seems to approve of the code, but it throws an error as soon as it is executed. It appears that the root cause lies in the fact that value is being declared without being initiali ...

The TypeScript error code TS2339 is indicating that the 'modal' property is not recognized on the type 'JQuery'

I'm currently utilizing Typescript with AngularJS and have encountered an issue with modals when using the typed definition of jQuery library. The specific error message I am receiving is: 'error TS2339: Property 'modal' does not exist ...

Is it possible to utilize [key:string]: any in order to eliminate the requirement for an additional function when working

Currently, I am utilizing recompose within my React project. Specifically, I am leveraging its compose function which is defined as: function compose<TInner, TOutter>( ...functions: Function[] ): ComponentEnhancer<TInner, TOutter>; interf ...

Oops! Looks like there's an unexpected error with the module 'AppRoutingModule' that was declared in the 'AppModule'. Make sure to add a @Pipe/@Directive/@Component annotation

I am trying to create a ticket, but I encountered an error. I am currently stuck in this situation and receiving the following error message: Uncaught Error: Unexpected module 'AppRoutingModule' declared by the module 'AppModule'. Plea ...