Hvordan bruke graphql fetch i TypeScript


Jeg så dette spørsmålet som er veldig nær det jeg prøver å oppnå, men det fungerte ikke helt for meg fordi endepunktet er et GraphQL-endepunkt og det finnes en annen innbyrdes egenskap med navnet på forespørselen. For eksempel, hvis min forespørsel er:

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

Da er objektet som hentes fra svaret lenket ovenfor data.data.users, der den siste egenskapen kommer fra navnet på GraphQL-spørringen selv. Jeg klarte å endre koden fra lenken ovenfor til følgende:

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

...som fungerer når jeg bare sender en enkelt forespørsel til GraphQL-endepunktet. Hvordan kan jeg generalisere slik at det vil fungere for et hvilket som helst antall forespørsler? For eksempel, hva om jeg vet at forespørselen min vil returnere User[] og Post[], som en tuppel, på grunn av følgende GraphQL-forespørsel:

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

Da vil jeg gjerne at noe som følgende skal fungere:

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

Er noe sånt mulig?

Svar #1

Akkurat nå ville jeg sagt at problemet ditt er dette...

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

Det vil bare noen gang returnere den første verdien fra data.

Jeg vil likevel fraråde å bruke tupler for dette. I stedet, returner bare data-objektet typet til forventet respons.

La oss starte med en generell objekt-type for å representere GraphQL data

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

Dette beskriver den mest generiske formen data kan ta. Det er i prinsippet et vanlig objekt med strengnøkler. never på numeriske indekser hindrer det i å være et array.

Deretter la oss beskrive GraphQL-responsformen

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

Dette representerer respons-JSON du får fra GraphQL, inkludert den tidligere GraphQlData-typen eller noe som spesialiserer seg på det. For eksempel, du kan spesifisere en bestemt responstype som...

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

Her er { users: Users[], posts: Posts[] } en mer spesialisert versjon av GraphQlData med nøkler begrenset til users og posts og spesifikke verdityper.

Til slutt, definer funksjonen, og innarbeid den samme generiske data-typen

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

Da kan du lage forespørselen din slik

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

Ellers vil du ønske å hente alle data verdiene, og hvis det er mer enn én, returner tuplen din i stedet for en enkelt post.

For å støtte en slik generisk returtype, må du spesifisere T som en union av enkeltype eller array-type.

Merk: Det er en iboende risiko her for at dataverdiene ikke er i den rekkefølgen du spesifiserer. Det er mye bedre å bruke verdiene etter nøkkel.

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

Relaterte artikler