December 8, 2016

An enhanced GraphQL developer experience with TypeScript

Robin Ricard

Robin Ricard

Guest post by Robin Ricard (GithubTwitterWebsite), community contributor to the Apollo project.

A few months ago, Sashko Stubailo wrote an excellent article about the benefits of static GraphQL queries. After seeing that article, I immediately wanted to get that great development experience he was talking about in my own projects. Unfortunately, all of this tooling was only available for Swift and Xcode, until now…

A complete development experience based on GraphQL types

Today, I’m happy to announce that a complete GraphQL developer experience for Typescript is available! (with Flow coming soon…)


Automatic query and fragments loading

In my latest job, I worked on a large GraphQL project where we had a lot of fragments. This rapidly became a pain to work with. We had many issues to correctly import those fragments in files scattered around the project. This made most of our fragment resolution not static at all. <a href="https://github.com/apollostack/graphql-document-collector" target="_blank" rel="noreferrer noopener">graphql-document-collector</a> has been created to solve this problem: instead of storing queries and fragments in js files, we can now store them in static graphql files around the project. The collector will gather all documents (fragments, queries, mutations, etc) found in the project and put them in a single file containing all of the project documents with their fragments auto-resolved!

This is usually done using a single command:

graphql-document-collector '**/*.graphql' > documents.json

This JSON file contains all of the documents you may want to use, let’s see how I would use that in my project:

// ...
import {graphql} from 'react-apollo';
const graphqlDocuments = require('./documents.json');

// ...

const FeedWithData = graphql(graphqlDocuments['Feed.graphql'])(Feed);

// ...

That’s pretty straightforward; you don’t even need to use the gql tag. In this case, Feed.graphql can use fragments located in a file named fragments/Item.graphql: the collector will handle that for us. The only limitation is that you can’t have two fragments with the same name in your project.

Automatic type annotation generation for TypeScript

Another issue we had was in guaranteeing the data we were accessing in our UI was corresponding to the query we just had fetched, even when we changed our queries in our codebase. Alongside our operations and fragments we added type annotations corresponding to the graphql document. Unfortunately, the annotations and the queries stopped matching each other over time. Either we forgot to report a field removal or we just misinterpreted the type from the schema, for instance, making something non-nullable that should be nullable.

Since GraphQL is a completely typed language and our queries are now completely static, we can now generate those annotations automatically! In order to do that in a TypeScript project, I added to <a href="https://github.com/apollostack/apollo-codegen" target="_blank" rel="noreferrer noopener">apollo-codegen</a> a generator that converts GraphQL documents and schema to typescript interfaces.

Let’s take this simple query with a fragment:

fragment DescribeHero on Character {
  name
  appearsIn
}

query HeroName($episode: Episode) {
  hero(episode: $episode) {
    ...DescribeHero
  }
}

With the following schema:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]
}

type Query {
  hero(episode: Episode): Character!
}

With those, using apollo-codegen for TypeScript, we can generate this annotations file:

//  This file was automatically generated and should not be edited.

// The episodes in the Star Wars trilogy
export type Episode =
  "NEWHOPE" | // Star Wars Episode IV: A New Hope, released in 1977.
  "EMPIRE" | // Star Wars Episode V: The Empire Strikes Back, released in 1980.
  "JEDI"; // Star Wars Episode VI: Return of the Jedi, released in 1983.


export interface HeroNameQueryVariables {
  episode: Episode | null;
}

export interface HeroNameQuery {
  hero: DescribeHeroFragment;
}

export interface DescribeHeroFragment {
  name: string;
  appearsIn: Array< Episode | null >;
}

All you have to do is pull your schema with an introspection query from your server:

apollo-codegen download http://localhost:8080/graphql \
  --output schema.json

Once you have the schema, generate the annotations:

apollo-codegen generate **/*.graphql \
  --schema schema.json \
  --target ts \
  --output schema.ts

You can now import and use those annotations in your codebase to ensure your GraphQL results are used safely. For instance, now, if I try to type this:

import {
  HeroNameQueryVariables,
  HeroNameQuery,
} from './schema';

// ...

const variables: HeroNameQueryVariables = {
  episode: 'JARJAR',
};
// [ts] Type '{ episode: "JARJAR"; }' is not assignable to type 'HeroNameQueryVariables'.
//      Types of property 'episode' are incompatible.
//      Type '"JARJAR"' is not assignable to type 'Episode'.

client.query({
  query: graphqlDocuments['DescribeHero.graphql'],
  variables,
})
.then(({data}: {data: HeroNameQuery}) => {
  data.hero.friends.forEach(friend => console.log(friend.name));
  // [ts] Property 'friends' does not exist on type 'DescribeHeroFragment'.
});

I’ll can see errors telling me I made some mistakes.

Create correct GraphQL documents from your editor

Since we just downloaded a schema.json, it’s good to know we can use it to type check our GraphQL documents as well. Just use the GraphQL ESLint plugin against your .graphql documents! (you will need to configure your editor to consider graphql files as javascript files.) You can then write correct queries from the start without having to type them in GraphiQL since ESLint will show you errors directly:

fragment DescribeHero on Character {
  name
  appearsIn
  isJarJar
  # [eslint] Cannot query field "isJarJar" on type "Character". (graphql/template-strings)
}

query HeroName {
  hero(episode: JARJAR) {
  # [eslint] Argument "episode" has invalid value JARJAR.
  #          Expected type "Episode", found JARJAR. (graphql/template-strings)
    ...DescribeHero
  }
}

Wrapping up!

As you can see, your development experience can be improved using static GraphQL documents. For now, though, all of this requires some important setup work that can be daunting at first. We’ll work on streamlining this process soon. In the meantime, you can always try our sample repository with all of the tooling you need to try out those features: <a href="https://github.com/apollostack/typed-graphql-client-example" target="_blank" rel="noreferrer noopener">typed-graphql-client-example</a>. You should try to open it with VSCode to get the best out of the type system.

Have fun developing awesome GraphQL apps!

Written by

Robin Ricard

Robin Ricard

Read more by Robin Ricard