Reputation: 191
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
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