GraphQL + Typescript: Strongly Typed API Responses
🎊 V1.0.0 has been released 🎊. Read this post to learn more.
Over at Avant, I’ve been working on removing some of our internal tools from our existing monolithic Rails application and putting them into services.
In the early stages of the move, we decided on using GraphQL to power our communication between services. Although it was still relatively new (outside of Facebook, that is) we felt that it would speed up our development on our mostly frontend application.
Fast forward a few months and our service is now powered by GraphQL. Now, however, I am in the process of building an app using TypeScript that largely consumes this API.
After a while my client looked something like this:
const GRAPHQL_URL: string = 'https://example';
const CONSUMER_QUERY: string = 'query { customer(id: 1) { id } }';function client(query: string): Promise<IResponse> {
return fetch(GRAPHQL_URL, {
method: 'POST',
body: JSON.stringify({ query })
});
}async function consumer (): void {
let response: any = await client(CONSUMER_QUERY); // => null
console.log(response.data.customer.notARealField); // => Uncaught TypeError: Cannot read property ‘id’ of undefined
console.log(response.data.customer.notARealField.id);
}
Considering GraphQL and TypeScript are both Strongly Typed, I shouldn’t be having to use an any type. Nor did I want to write interfaces for every expected response.
I did some Googling and did not find any posts about this, so I decided to spend some time on it and fix it.
Because of its awesome introspection, GraphQL makes it simple to get the graph’s schema. The graphql-js reference implementation includes an Introspection Query to get all relevant information from the graph.
Using this introspection query, we can arrive at a JSON response like:
Luckily, this response contains all of the relevant information we need to create interfaces for use in TypeScript.
So I wrote a utility named gql2ts to convert this schema to TypeScript definitions. This is the output from the sample schema above:
Awesome. Now we have some more type safety in our API responses.
import GQL from 'gql';const GRAPHQL_URL: string = 'https://example';
const CONSUMER_QUERY: string = 'query { customer(id: 1) { id } }';function client(query: string): Promise<GQL.IGraphQLResponseRoot> {
return fetch(GRAPHQL_URL, {
method: ‘POST’,
body: JSON.stringify({ query })
});
}async function consumer (): void {
let response: GQL.IGraphQLResponseRoot = await client(CONSUMER_QUERY); // => works!
let foo: string = response.data.customer.id; // => doesn't compile
let bar: number = response.data.customer.id; // => doesn't compile
console.log(response.data.customer.notARealField); // => doesn't compile
console.log(response.data.customer.notARealField.id);
}
Obviously this is an extremely contrived example, but I hope the utility can be seen.
If you’re interested, gql2ts can be installed via npm:
$ npm install -g gql2ts
It’s run very simply:
$ gql2ts schema.json
By default, this will output a file named graphqlInterfaces.d.ts with a module named GQL.
Optionally you can ignore certain types, use a custom module name, and write to a different file. For instance:
$ gql2ts -m AwesomeModule -i Customer -o awesome.d.ts schema.json
There’s still some more stuff to do, but I think this is a good starting point and hopefully will help some others.