JSCodeshift transform that inserts Flow types generated from GraphQL documents in template string literals and your GraphQL schema, and wires up the generated types to Apollo
- Example
-
Rationale
- Importing generated types from external files is annoying
graphql-codegen
outputs messy types for documents- I want to extract parts of the query with their own type aliases
- Interpolation in GraphQL tagged template literals
- Automatically adding type annotations to
Query
,Mutation
,useQuery
,useMutation
, anduseSubscription
- Configuration
- CLI Usage
- Node.js API
import gql from 'graphql-tag'
const fragment = gql`
fragment CharacterFields on Character {
name
appearsIn
}
`
const fragment2 = gql`
${fragment}
fragment CharacterAndFriends on Character {
...CharacterFields
friends {
...CharacterFields
}
}
`
const query = gql`
${fragment2}
query Test($id: ID!) {
character(id: $id) {
id
...CharacterAndFriends
}
}
`
import gql from 'graphql-tag'
const fragment = gql`
fragment CharacterFields on Character {
name
appearsIn
... on Human {
mass
}
... on Droid {
primaryFunction
}
}
`
// @graphql-typegen auto-generated
type CharacterFields =
| {
name: string,
appearsIn: Array<?('NEWHOPE' | 'EMPIRE' | 'JEDI')>,
__typename: 'Human',
mass: ?number,
}
| {
name: string,
appearsIn: Array<?('NEWHOPE' | 'EMPIRE' | 'JEDI')>,
__typename: 'Droid',
primaryFunction: ?string,
}
const fragment2 = gql`
${fragment}
fragment CharacterAndFriends on Character {
...CharacterFields
friends {
...CharacterFields
}
}
`
// @graphql-typegen auto-generated
type CharacterAndFriends = {
__typename: 'Human' | 'Droid',
friends: ?Array<?CharacterFields>,
} & CharacterFields
const query = gql`
${fragment2}
query Test($id: ID!) {
character(id: $id) {
id
...CharacterAndFriends
}
}
`
// @graphql-typegen auto-generated
type TestQueryVariables = { id: string }
// @graphql-typegen auto-generated
type TestQueryData = {
__typename: 'Query',
character: ?({
__typename: 'Human' | 'Droid',
id: string,
} & CharacterAndFriends),
}
With established GraphQL code generators out there (apollo-tooling
and graphql-codegen
)
you might wonder why I decided to make my own instead. There are several reasons...
Both graphql-codegen
and apollo-tooling
output types in separate files from your
GraphQL documents. This means you have to pick a globally unique identifier for
each GraphQL operation. Dealing with global namespaces is always a pain in the ass.
It also means you have to insert an import statement. While I also have my own tool
that's pretty damn good at automatic imports,
it would still be an annoying extra step.
graphql-typegen
just inserts the generated types after the GraphQL
tagged template literals in my code, so I don't have to worry about picking
globally unique operation names or adding imports.
Example:
query Test($id: ID!) {
user(id: $id) {
id
username
}
}
Output:
type $Pick<Origin: Object, Keys: Object> = $ObjMapi<
Keys,
<Key>(k: Key) => $ElementType<Origin, Key>
>
export type TestQueryVariables = {
id: $ElementType<Scalars, 'ID'>,
}
export type TestQuery = {
...{ __typename?: 'Query' },
...{|
user: ?{
...{ __typename?: 'User' },
...$Pick<User, {| id: *, username: * |}>,
},
|},
}
Pretty awful, huh? It's questionable if this even works properly in Flow; I've seen bugs with spreads inside inexact/ambiguous object types in the past.
graphql-typegen
would output:
// @graphql-typegen auto-generated
type TestQueryData = {
__typename: 'Query',
user: {
__typename: 'User',
id: ID,
username: string,
},
}
Take the query example above. Let's say I need to refer to the user
type in TestQuery
above. All I have to do is add this comment:
query Test($id: ID!) {
# @graphql-typegen extract
user(id: $id) {
id
username
}
}
And graphql-typegen
will output:
// @graphql-typegen auto-generated
type User = {
__typename: 'User',
id: ID,
username: string,
}
// @graphql-typegen auto-generated
type TestQueryData = {
__typename: 'Query',
user: User,
}
This is much easier than type User = $PropertyType<TestQuery, 'user'>
,
especially for extracting types that are more than one level deep in the query
($PropertyType<$PropertyType<TestQuery, 'foo'>, 'bar'>
would be pretty awful)
At the moment, apollo-tooling
doesn't support interpolation in tagged template literals.
This is a pretty crucial for sharing fragments between queries and mutations, which is, you know, common.
graphql-typegen
supports this:
const UserFragment = gql`
fragment UserFields on User {
id
username
}
`
const userQuery = gql`
${UserFragment}
query user($id: ID!) {
user(id: $id) {
...UserFields
}
}
`
const updateUserMutation = gql`
${UserFragment}
mutation updateUser($id: ID!, $values: UpdateUser!) {
updateUser(id: $id, values: $values) {
...UserFields
}
}
`
Output:
// @graphql-typegen auto-generated
type UserFields = {
id: ID,
username: string,
}
// @graphql-typegen auto-generated
type UserQueryData = {
__typename: 'Query',
user: { __typename: 'User' } & UserFields,
}
// @graphql-typegen auto-generated
type UserQueryVariables = {
id: ID,
}
// @graphql-typegen auto-generated
type UpdateUserMutationData = {
__typename: 'Mutation',
updateUser: { __typename: 'User' } & UserFields,
}
// @graphql-typegen auto-generated
type UpdateUserMutationVariables = {
id: ID,
values: {
username?: string,
},
}
graphql-typegen
also supports string interpolation:
const userFields = `
id
username
`
const userQuery = gql`
query user($id: ID!) {
user(id: $id) {
${userFields}
}
}
`
const updateUserMutation = gql`
mutation updateUser($id: ID!, $values: UpdateUser!) {
updateUser(id: $id, values: $values) {
${userFields}
}
}
`
Output:
// @graphql-typegen auto-generated
type UserQueryData = {
__typename: 'Query',
user: {
__typename: 'User',
id: ID,
username: string,
},
}
// @graphql-typegen auto-generated
type UserQueryVariables = {
id: ID,
}
// @graphql-typegen auto-generated
type UpdateUserMutationData = {
__typename: 'Mutation',
updateUser: {
__typename: 'User',
id: ID,
username: string,
},
}
// @graphql-typegen auto-generated
type UpdateUserMutationVariables = {
id: ID,
values: {
username?: string,
},
}
Automatically adding type annotations to Query
, Mutation
, useQuery
, useMutation
, and useSubscription
graphql-typegen
will analyze all calls to these hooks and add the correct type annotations:
const userQuery = gql`
query user($id: ID!) {
user(id: $id) {
id
username
}
}
`
const Foo = ({ id }: { id: ID }): React.Node => {
const { data } = useQuery(userQuery, { variables: { id } })
return <pre>{JSON.stringify(data)}</pre>
}
graphql-typegen
inserts the type parameters useQuery<UserQueryData, UserQueryVariables>
.
const userQuery = gql`
query user($id: ID!) {
user(id: $id) {
id
username
}
}
`
// @graphql-typegen auto-generated
type UserQueryData = {
__typename: 'Query',
user: {
__typename: 'User',
id: ID,
username: string,
},
}
// @graphql-typegen auto-generated
type UserQueryVariables = {
id: ID,
}
const Foo = ({id}: {id: ID}): React.Node => {
const {data} = useQuery<UserQueryData, UserQueryVariables>(userQuery, {variables: {id}})
return <pre>{JSON.stringify(data)}</pre>
}
First, you need to add the following to your package.json
to tell graphql-typegen
how to
find your schema:
"graphql-typegen": {
"schemaFile": "path/to/schema.graphql"
}
Or
"graphql-typegen": {
"server": "http://localhost:4000/graphql"
}
Name of the template literal tag used to identify template literals containing GraphQL queries in Javascript/Typescript code
Configure this in your package.json
:
"graphql-typegen": {
"tagName": "gql"
}
Whether to validate each GraphQL document before processing it.
Excludes NoUnusedFragmentsRule
from validation, in case you put fragment definitions
in separate template literals.
Right now this is only configurable in your package.json
:
"graphql-typegen": {
"validate": false
}
Places this may be configured, in order of increasing precendence:
"graphql-typegen": {
"addTypename": false
}
"""
@graphql-typegen noTypename
"""
type User {
id: ID!
name: String!
}
const query = gql`
query user($id: Int!) {
# @graphql-typegen addTypename
user(id: $id) {
id
name
}
}
`
The type of Flow object to output, one of:
exact
inexact
ambiguous
Places this may be configured, in order of increasing precendence:
"graphql-typegen": {
"objectType": "exact"
}
"""
@graphql-typegen exact
"""
type User {
id: ID!
name: String!
}
const query = gql`
query user($id: Int!) {
# @graphql-typegen exact
user(id: $id) {
id
name
}
}
`
Whether to use readonly object and array types.
Places this may be configured, in order of increasing precendence:
"graphql-typegen": {
"useReadOnlyTypes": true
}
"""
@graphql-typegen readOnly
"""
type User {
id: ID!
name: String!
}
const query = gql`
query user($id: Int!) {
# @graphql-typegen mutable
user(id: $id) {
id
name
}
}
`
Whether to annotate useQuery
, useMutation
and useSubscription
calls with type arguments,
or annotate the input variables and output data.
Configure this in your package.json
:
"graphql-typegen": {
"useFunctionTypeArguments": false
}
Adds <QueryData, QueryVariables>
to useQuery
:
const {loading, error, data} = useQuery<QueryData, QueryVariables>(query, {
variables: {id}
})
Annotates this way:
const { loading, error, data }: QueryRenderProps<QueryData, QueryVariables> =
useQuery(query, {
variables: ({ id }: QueryVariables),
})
Makes graphql-typegen
use the given external type for a scalar.
Places this may be configured, in order of increasing precendence:
"""
@graphql-typegen external as import { type DateISOString } from './src/types/DateISOString'
"""
scalar DateTime
const query = gql`
query user($id: Int!) {
user(id: $id) {
id
# @graphql-typegen external as string
createdAt
}
}
`
Makes graphql-typegen
extract the given type or field's inner type into a type alias,
instead of generating an inline type.
There may be funky behavior if a selection set with inline fragment spreads is extracted.
The name of the type is used if you don't specify as .
Places this may be configured, in order of increasing precendence:
"""
@graphql-typegen extract
"""
type User {
}
const query = gql`
query user($id: Int!) {
# @graphql-typegen extract as User
user(id: $id) {
id
name
}
}
`
// @graphql-typegen auto-generated
type UserQueryData = {
__typename: 'Query',
user: ?{
id: string,
name: string,
},
}
// @graphql-typegen auto-generated
type UserQueryData = {
__typename: 'Query',
user: ?User,
}
// @graphql-typegen auto-generated
type User = {
id: string,
name: string,
}
ignore data
makes graphql-typegen
not output a data type.
ignore variables
makes graphql-typegen
not output a variables type.
ignore
makes graphql-typegen
not output either a data or variables type.
This may be set in a comment in your GraphQL operation:
const query = gql`
# @graphql-typegen ignore variables
query user($id: Int!) {
user(id: $id) {
id
name
}
}
`
jscodeshift -t path/to/graphql-typegen/graphql-typegen.js src/**/*.js
Because jscodeshift
unfortunately requires transform functions to be sync,
graphql-typegen
uses an execFileSync
hack to synchronously fetch your schema
from your schema file or server.
If you're calling directly from node, you can bypass this by using graphql-typegen-async
:
import graphqlTypegen from 'graphql-typegen/graphql-typegen-async'
It has the same API as a jscodeshift
transform, except that it returns a Promise
instead
of a sync result. Maybe someday jscodeshift
will support async transforms.