Skip to content

[TypeScript] Proposal: unified output for clients #1777

@hallettj

Description

@hallettj

Currently there are a number of specialized plugins and plugin configurations to produce code for different client-side environments. This is a proposal to generalize generated code so that one form of output can serve the needs of any client environment.

For example the typescript/react-apollo plugin can emit components or hooks (or HOCs):

// React Component
export const GetPostsComponent = (
  props: Omit<
    Omit<
      ReactApollo.QueryProps<GetPostsQuery, GetPostsQueryVariables>,
      "query"
    >,
    "variables"
  > & { variables?: GetPostsQueryVariables }
) => (
  <ReactApollo.Query<GetPostsQuery, GetPostsQueryVariables>
    query={GetPostsDocument}
    {...props}
  />
)

// React hook
export function useGetPostsQuery(
  baseOptions?: ReactApolloHooks.QueryHookOptions<GetPostsQueryVariables>
) {
  return ReactApolloHooks.useQuery<GetPostsQuery, GetPostsQueryVariables>(
    GetPostsDocument,
    baseOptions
  )
}

It is possible to generalize to reduce the surface area of code generation. Instead of emitting functions with type parameters filled in we could emit GraphQL document nodes that carry type parameters with them like this:

interface TypedDocumentNode<_TData, _TVariables> extends DocumentNode {}

export const getPostsQuery: TypedDocumentNode<GetPostsQuery, GetPostsQueryVariables> = GetPostsDocument

This would require libraries that consume a DocumentNode to change to accept a compatible type that carries parameters (the TypedDocumentNode type in the example above.) For example the signature for react-apollo-hooks' useQuery function would change to look like this:

export function useQuery<
  TData = any,
  TVariables = OperationVariables,
  TCache = object
>(
  query: TypedDocumentNode<TData, TVariables>,
  options?: QueryHookOptions<TVariables, TCache>
): QueryHookResult<TData, TVariables>

And here is an example of usage:

import { useQuery } from "react-apollo-hooks"
import { getPostsQuery } from "./generated/graphql.ts"

export function App(props: { postId: string }) {
  const { data, error, loading } = useQuery(getPostsQuery, {
    variables: { postId },
  })
  
  /* ... */
}

The type of getPostsQuery comes with parameters that flow into useQuery which allows Typescript to infer the correct types for data, and for the variables option to useQuery.

TypedDocumentNode is compatible with DocumentNode so it would still be possible to use this new version of useQuery with arguments that do not carry type parameters, such as the return value from an inline gql invocation.

A similar change can be made to react-apollo - changing the QueryProps type to accept a value of type TypedDocumentNode allows types to flow from a generated operation to the Query component:

import { Query } from "react-apollo"
import { getPostsQuery } from "./generated/graphql.tsx"

export function App(props: { postId: string }) {
  return (
    <Query query={getPostsQuery} variables={{ postId: props.postId }}>
      {({ data }) => (data ? <div>{data[0].title}</div> : "...")}
    </Query>
  )
}

Once again, type parameters flow from getPostsQuery to Query props which results in accurate type inference for data, and for the variables prop.

The result would be to dramatically reduce the burden on graphql-codegen plugins to accommodate different client-side libraries and configurations. I propose adding an operationValues configuration option to @graphql-codegen/typescript-operations to emit typed document node values.

This plan does come with some external dependencies:

  • There must be a type like TypedDocumentNode defined in some shared library - ideally in @types/graphql. (Or DocumentNode itself could be changed to add optional type parameters.)
  • Client libraries must be updated to use that new type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions