Over the last few years, the popularity of both GraphQL and TypeScript has exploded in the web ecosystem—and for good reason: They help developers solve real problems encountered when building modern web applications. One of the primary benefits of both technologies is their strongly typed nature.
Strong typing is a communication tool that uses explicit statements of intent. Method signatures tell you exactly what kind of input they expect and what kind of output they return. The compiler doesn't allow you to break those rules. It's about failing fast at compile time instead of by users at runtime. TypeScript is a strongly typed language that brings this to the JavaScript ecosystem.
GraphQL also brings this benefit to an area of web applications that is notoriously error-prone–interacting with backend APIs. By providing a schema both the server and client can depend on (because it is enforced by the specification), GraphQL provides a strongly typed “bridge” between both sides of the application.
Going forward, this post will assume the reader has a working knowledge of both GraphQL and TypeScript. If you don’t, that’s totally fine, and you should still be able to understand the concepts.
Let’s build an app about one of my favorite shows, Game of Thrones, so we have an example to work from. We’ll build a GraphQL server for the backend and a React app for the frontend, both written in TypeScript.
We’ll start with the server. To keep this example focused on GraphQL, we aren’t going to use a real database to store our data. Instead, we’ll hard-code the data to serve as an in-memory “database”. I’ve taken the liberty of writing the data functionality ahead of time, but if you’re curious, you can inspect all of the code in this repository.
First, we’ll get the boring boilerplate out of the way. We’ll spin up a server and provide the GraphQL context, which will be provided as an argument to all of our resolvers. It’s a good place to store user information, data models, etc. Pretty standard stuff. The details here aren’t pertinent to this post.
import{ApolloServer}from"apollo-server";import*as bookModelfrom"./models/book";import*as characterModelfrom"./models/character";import*as houseModelfrom"./models/house";import*as tvSeriesModelfrom"./models/tv-series";import{ resolvers }from"./resolvers";import{ schema }from"./schema";exportinterfaceContext{models:{character:typeof characterModel;house:typeof houseModel;tvSeries:typeof tvSeriesModel;book:typeof bookModel;};}constcontext:Context={models:{character: characterModel,house: houseModel,tvSeries: tvSeriesModel,book: bookModel
}};const server =newApolloServer({typeDefs: schema, resolvers, context
});server.listen().then(({ url })=>{console.log(`🚀 Server ready at ${url}`);});
When I’m working on a GraphQL server, I like to start by modeling the “domain” in the schema, and then later implement the resolvers and data fetching. Here’s what our Game of Thrones schema looks like:
A few TypeScript-related things should jump out at you. First, we have to define the properties of our resolver object so TypeScript knows what to expect (Query, Character, etc.). For each of those properties (resolver functions), we have to manually define the type definitions for all of the parameters.
The first argument, usually referred to as the “root” or “parent” parameter, has a different type depending on which resolver you are working on. In this example, we see any, Character, TvSeries, and House, depending on which parent type we are working with. The second argument contains the GraphQL query arguments, which is different for each resolver. In this example, some are any, some take are sortDirection, and some accept an ID. The third argument, context, is thankfully the same for every resolver.
If we want type-safe resolvers, we have to do this for every resolver. Exhausting.
And there’s a catch. If we decide to change our GraphQL schema by, for example, changing or adding a new argument, we have to remember to manually update the type definitions for the respective resolver. I don’t know about you, but the chances I’ll remember to do that are lower than 100%. Our code will compile, TypeScript will be happy, but we’ll get a runtime error.
Bummer.
(spoiler alert: we will solve this problem later in the post.)
Now that our server is done, let’s run the app and make sure it works.
Now that we have a server, let’s write a front-end application. We’ll use create-react-app to simplify the setup and Apollo Client for the GraphQL functionality. Our app will show a list of Game of Thrones characters with some basic information. If you click on a character, you will see more information about that character.
When I’m working on a component, I like to start with the data. The following GraphQL query will give us the data we need to render the character list.
Again, there are a few TypeScript-related things that will jump out at you. The loading and error properties are already auto-typed, which is great. Unfortunately, the data property is not. In order to have type-safe data, we need to manually define a TypeScript interface (based on what’s requested in the query—see Data and Character). Just like before, if we change the query, we have to remember to update the TypeScript interface.
What we have is pretty cool—GraphQL data-fetching and type-safe client and server applications. But we also have a big problem. There is a ton of duplication between the GraphQL schema and the TypeScript interfaces, requiring manual synchronization when changing our schema.
What we really want is for our GraphQL schema to be the single source of truth for our types.
Is there any way to accomplish this? As you can probably guess from the title of this post, the answer is yes.
GraphQL Code Generator is a tool built to solve this problem. By parsing and analyzing our GraphQL schema, it outputs a wide variety of TypeScript definitions we can use in our GraphQL resolvers and front-end components. It supports many output formats, but we will focus on the resolver definitions and the react-apollo component generation. That’s right, it can even generate fully typed React components for us.
Let’s start with generating type definitions for the resolvers. After reading the documentation, which is quite thorough, this is what I came up with:
schema:./server/schema.tsgenerates: server/gen-types.ts:config:defaultMapper: any
contextType:./#Contextplugins:- typescript
- typescript-resolvers
js
There are a few important pieces that I’ll explain. The schema field, as the name implies, tells GraphQL Code Generator where to find our schema. The generates field tells it where to place the generated type definitions, and the plugins array tells it which plugins to use when generating that file.
After running the tool with the above configuration, we now have this file. It’s pretty complex and uses a ton of TypeScript generics, but I recommend you dig around to see what it’s doing.
It’s pretty magical.
Using only our GraphQL schema, the tool automatically generated type definitions for all of our resolvers.
We can now replace all of the manual type definitions we wrote earlier with the generated types. Our modified resolver file is here, and you can view a before and after comparison below. Notice how many of the manual type definitions we were able to delete?
The benefits are already enormous, but it gets better. This workflow really shines when we need to make a change to our GraphQL schema. All we have to do is make the change, generate new types, and we’ll get type errors in all of the places that need to change. In this example, we removed the sortDirection parameter from the getHouses query.
GraphQL Code Generator will use the previously -configured schema from our server, as well as the client-side queries it finds in the queries directory, to generate type definitions and React components. Check out the generated file. Again, it’s pretty complex, but try to understand what it’s doing.
Using only our GraphQL queries, the tool automatically generated fully-typed react-apollo components that we can use in our application.
Again, this is huge. And as before, it’s worth its weight in gold when there is a schema or query change. They will be reflected in the generated types and you’ll immediately see what needs to be fixed.
GraphQL and TypeScript’s popularity explosion in the web ecosystem is, in my opinion, a Good Thing. They help developers solve real problems encountered when building and maintaining modern web applications. One of the primary benefits of both technologies is their strongly typed nature.
Since they are both independent type systems, however, there is a risk of duplication and divergence between the two. By treating GraphQL as the single source of truth for our types and generating TypeScript definitions from it, we diminish this risk. Luckily for us, amazing packages like GraphQL Code Generator exist!
While you can manually run the code generator after a schema or query change, you might forget. Instead, I recommend adding an npm script which monitors the appropriate files and runs the generator tool when it detects changes. You can run this command concurrently with your normal development workflow using the concurrently package.