3

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?

1 Answer 1

2

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;
}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for your thorough response. If I understand correctly, the bottom section of your code is if I insist on getting the data as a tuple, but you recommend the first section which gets it by keys (which is safer and recommended), correct? I don't mind getting it by keys if that's the case. Also, I'm a little new to typescript, could you elaborate on what all is going on in the first code block between the type and interface? Not exactly sure what Record is or why the type needs to be extended. Thanks!
That's right, I added the second example just to be thorough but strongly recommend the first. I'll edit my answer to explain the interface I'm using. FYI Record is just a utility type. It's like a plain object or map
@IsaacTorres I've updated my answer with extra details. I've also removed Record as I found a better way to represent the data
graphQLFetch needs to be declared as async
@NeilMorgan it already is 😕

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.