Automated TypeScript typing with GraphQL & Apollo

Tom Parsons
ITNEXT
Published in
4 min readDec 17, 2019

--

One approach to using TypeScript with GraphQL is to generate a TypeScript type library based on a GraphQL schema, creating interfaces for all of the objects we might use in the code, which does work fine.

However, there is a safer way to do this, and that is by having unique types for every situation, rather than shared types that can be used everywhere. In this article, I’ll explain what’s wrong with this approach as well as a mechanism on how to improve it.

All of the code for this is available here: https://github.com/thomasparsons/graph-apollo-typing

The first approach for generating and using types

One mechanism that is in place to generate types is using a similar method to theyarn generate-types call that lives in the native folder in the above project:

cd native && yarn generate-types

This converts a schema file generated from the graph into matching types so:

type DogInfo {
name: String!
age: String
}

Is converted to:

export type DogInfo = {
__typename?: 'DogInfo',
name: Scalars['String'],
age?: Maybe<Scalars['String']>,
};

Then in the code, there might be something like:

import {DogInfo} from "../generated/types"interface Props {
dogInfo: DogInfo
}
export default function DogInfo({dogInfo}: Props) {
const {age, name} = dogInfo
render (
<Text>{name}: {age}</Text>
)
}

How that ties in with Apollo

If the above was redux, it would be ok, maybe not perfect, but it would be ok… However, when using GraphQL, as the response is specific to the data requested, more precise typing is required, so let’s look at a query; in this instance, we’re expecting an array of Dogs, with their age and name.

query getDogs {
getDogs {
dogInfo {
age
name
}
}
}

In which case, the original typing would be fine, however, if the property: age was removed from the query, as perhaps only the name is required, then some problems may occur. As the typing still has the age value on the interface, an engineer changing code later on, may try and render that age value and it wouldn’t be clear right away that it will be problematic.

const {age, name} = dogInforender (
<Text>{name}: {age}</Text> // this won't work as expected!
)

What if the types were generated from the queries?

Rather than using the above modal, which could lead to some problems, there is an alternative approach…

yarn generate-query-typesor:apollo codegen:generate generated --localSchemaFile=./generated/schema.json --target=typescript

Essentially, this will generate a unique set of types for each query (mutation, etc.) so that a generated type could be imported, a bit like this:

import {getDogs_getDogs_dogInfo} from "../Queries/generated/getDogsinterface Props {
dogInfo: getDogs_getDogs_dogInfo
}
export default function DogInfo({dogInfo}: Props) {
const {age, name} = dogInfo
render (
<Text>{name}: {age}</Text>
)
}

NB: the actual component logic doesn’t necessarily change

In the IDE or CI, it’ll quickly become apparent that if the age value isn’t requested in the query, there will be a lint error as the value isn’t in the type. If the age value was then added, and the types updated, the linter would pass.

Equally, if a new value was added to the query that isn’t yet available on the graph, then the GraphQL lint will fail, and the Apollo type generator will also fail.

The downsides

There are a couple of negatives…

  • It’s ugly. Make no bones about it, import {getDogs_getDogs_dogInfo} from "../Queries/generated" isn’t the most attractive way of writing code…
  • It requires a lot of script running to keep everything up to date, notably, the apollo codegen command to keep your output types from your queries up to date. (You can use the --watch command to save some hassle)
  • I had to turn the camelcase rule off in eslint as there currently isn’t a way of removing the underscores without using a different package (i.e https://graphql-code-generator.com/)

Is there still a place for the general types?

There most likely is a need for the system that is in place now, however, not when fetching data from the Graph, which would probably account for maybe 80% of component data, with the other 20% being stateful management, etc. For which there would, of course, be a necessity for typing, though one might make the argument that forms, state, and mutations, should probably use or extend the automated mutation type.

--

--