apollographql / apollo-feature-requests

🧑‍🚀 Apollo Client Feature Requests | (no 🐛 please).
Other
130 stars 7 forks source link

Allow embedding the types of the GraphQL document so they can be inferred #114

Open lemonmade opened 5 years ago

lemonmade commented 5 years ago

Migrated from: apollographql/react-apollo#3054

danielkcz commented 5 years ago

My solution to this is to use graphql-code-generator. I have some custom templates, but that's a detail. As the output, I get a code like this...

export const QUserMenuLanguagesDocument = gql`
  query QUserMenuLanguages {
    languages {
      code
      enum
      rawCode
    }
  }
`

export function useQUserMenuLanguages(
  variables?: QUserMenuLanguagesVariables,
  baseOptions?: Hooks.QueryHookOptions<QUserMenuLanguagesVariables>,
) {
  return Hooks.useQuery<QUserMenuLanguagesQuery, QUserMenuLanguagesVariables>(
    QUserMenuLanguagesDocument,
    variables,
    baseOptions,
  )
}

That way I don't need to care about types at all because the useQUserMenuLanguages is already fully typed and everything that comes from it as well.

lemonmade commented 5 years ago

Hey @FredyC, thanks for commenting! That solution definitely works, but it's not ideal from my perspective. If I was dealing with only a few GraphQL documents, it would be totally doable, but I'm dealing with apps that have dozens or hundreds of documents. Forcing every developer to always remember to get the right variables and data types, and create a separate hook for their query, is not ideal. Additionally, we use the documents in more than just app code; in test code, we use it to provide types that are used to validate a "partial data" argument:

fillGraphQL(myQuery, {foo: 'bar'});
//                   ^ should be a valid partial version of myQuery’s data

fillGraphQL(myQuery, ({id}) => ({id}));
//.                   ^ should align with the type of myQuery’s variables

We could construct an extra "filler" for each query, too, but you can see how it really balloons. We also sometimes use the document types in a location where you can't provide a typed alternative. One such spot is custom assertions in test, where we validate that queries are called with particular arguments, and want to make sure the variables passed are valid given the known types of the query itself:

expect(graphQL).toHavePerformedGraphQLOperation(myQuery, {id: '123'});
//                                                       ^ should be a partial of the allowed variables
oeed commented 4 years ago

I've also been doing this by overriding the type modules (but obviously, that's not pleasant/good practice). I've found that it essentially works 'for free' - but I might be missing something, I'd be curious to know if I am.

In the same way as @lemonmade did, DocumentNode takes the data and variables types as generics. All the functions that consume a DocumentNode can link generics to its data and variables, automatically inferring the needed arguments (but this is entirely optional as rather than DocumentNode consumer setting the defaults, the definition of DocumentNode does it. In this example I've simply overridden the definitions so anyone can try it, but ideally it'd be native to the definitions. Obviously it'd need some tweaks to make variables required when not undefined.

declare module "graphql" {
  import { OperationVariables } from '@apollo/react-common';
  import { DocumentNode as OriginalDocumentNode } from "graphql/index";
  export * from "graphql/index";

  export interface DocumentNode<TData = any, TVariables = OperationVariables> extends OriginalDocumentNode {}
}

declare module "@apollo/react-hooks" {
  export * from "@apollo/react-hooks/lib/index";
  import { QueryResult } from '@apollo/react-common';
  import { QueryHookOptions } from '@apollo/react-hooks/lib/types';
  import { DocumentNode } from 'graphql';

  export function useQuery<TData, TVariables>(query: DocumentNode<TData, TVariables>, options?: QueryHookOptions<TData, TVariables>): QueryResult<TData, TVariables>;

  // etc...

}

Then when you use it TypeScript infers the types for you. It works perfectly fine with manually specifying them too from my testing.

// your query and its types from whatever generator
export const myQuery: DocumentNode<MyQuery, MyQueryVariables> = gql`
  ...
`

export const MyComponent: FunctionComponent = () => {
  // both 'data' and 'variables' have MyQuery, MyQueryVariables automatically
  const { data } = useQuery(myQuery, {
    variables: {
      ...
    }
  })

  if (data) {
    return <p>Count: { data.count }</p>
  }
  return null
}
lishine commented 4 years ago

My solution to this is to use graphql-code-generator. I have some custom templates, but that's a detail. As the output, I get a code like this...

export const QUserMenuLanguagesDocument = gql`
  query QUserMenuLanguages {
    languages {
      code
      enum
      rawCode
    }
  }
`

export function useQUserMenuLanguages(
  variables?: QUserMenuLanguagesVariables,
  baseOptions?: Hooks.QueryHookOptions<QUserMenuLanguagesVariables>,
) {
  return Hooks.useQuery<QUserMenuLanguagesQuery, QUserMenuLanguagesVariables>(
    QUserMenuLanguagesDocument,
    variables,
    baseOptions,
  )
}

That way I don't need to care about types at all because the useQUserMenuLanguages is already fully typed and everything that comes from it as well.

This is autogenerated? Please share your setup