Isaac Torres
Isaac Torres

Reputation: 191

How to use graphql fetch in TypeScript

I saw this question which is very close to what I'm trying to accomplish, but it didn't quite work for me because the endpoint is a graphQL endpoint and there's another nested property by the name of the query. For example, if my query is:

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

Then the fetched object from the above linked answer is data.data.users, where the last property comes from the graphql query name itself. I was able to modify the code from the above link to the following:

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

...which works when I'm only providing a single query to the graphQL endpoint. How could I generalize so that it would work for whatever number of queries? For example, what if I know my query will return User[] and Post[], as a tuple, due to the following graphql query:

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

Then I would like something like the following to work:

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

Is something like this possible?

Upvotes: 3

Views: 2740

Answers (1)

Phil
Phil

Reputation: 164731

Right now I'd say your problem is this...

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

That will only ever return the first value from data.

I'd advise against tuples for this though. Instead, just return the data object typed to your expected response.

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

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

This describes the most generic form the data can take. It's basically a plain object with string keys. The never on numeric indexes prevents it from being an array.

Next, let's describe the GraphQL response form

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

This represents the response JSON you get from GraphQL, including the previous GraphQlData type or anything that specialises on that. For example, you could specify a particular response type as...

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

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

Finally, define 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}`);
  }

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

Then you can make your request like this

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

Otherwise, you'll want to get all the data values and if more than one, return your tuple instead of a singular record.

In order to support such a generic return type, you'll need to specify T as a union of singular or array type.

Note: There's an inherent risk here that the data values are not in the order you specify. It's much better to use the values by key.

// Note there's no generic type now
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;
}

Upvotes: 2

Related Questions