Patrick Desjardins Blog
Patrick Desjardins picture from a conference

How to automatically generate TypeScript for consumer of your GraphQL

Posted on: 2019-04-02

One strength of GraphQL is that it has types. In an ideal world, the consumer of the GraphQL would receive the payload strongly typed. The reason that it is not the case is natural. The data that is moving from the server to the client who invoked the query receives a JSON format that does not have any notion of type. However, the client who call the GraphQL know exactly which fields he wants to consume and GraphQL maps each queried fields to a type.

Objectives

The idea of this article is to let the user who call the GraphQL server to get a tailored object that is only for the field requested but also strongly typed for the field needed. It means that if the user request field A, B, and C of an object that has A, B,C, D, E, that the type that will be automatically generated will be only with A, B, C. While it might look that the generated type is not representing the real object, it reflects exactly the data that can be used. There is no need to have a generated object that you could not use.

Constraints

Many articles discuss about introspection and how types can be generated by having the consumer connect directly to GraphQL server to fetch the schema. I had the constraint that I could not do it for security reason, hence I had to expose the schema by the server into another way and have the consumer uses the generated merged schemas.

Server Steps

On the server side, one step is required: exposing the schemas into a unified schema (file) that can be shared. By generating the type every time the server is compiled, the schema remains in synchronization. Hence, we do not lose the freshness of being connected directly to GraphQL.

I created a script in the package.json that execute a TypeScript script.

"generateunifiedschema": "ts-node src/scripts/mergeAllGraphqlSchemas.ts",

The code in the TypeScript file is simple -- really simple. Here is the whole code:

import fs from "fs";
import { fileLoader, mergeTypes } from "merge-graphql-schemas";
import path from "path";
const documentTypeDefs = fileLoader("src/**/*.graphql");
const mergedSchema = mergeTypes(documentTypeDefs);
const typesPath = "output_types";
const schemaPath = path.join(typesPath, "allSchema.graphql");
fs.writeFileSync(schemaPath, mergedSchema);

It uses the library merge-graphql-schemas and search for all individual schema that is spread inside the project. The result goes outside in a folder. That's it. In my case, that generated file is available internal at Netflix but not exposed outside. It means that every developers that consume the GraphQL service can download the schema, create query in their TypeScript code and use the unified schema to generate their code.

Client Steps

Each consumer will have to do few steps. First, they need to download the unified schemas. Second, they need to write their GraphQL query and use gql. The gql allows to have a library that will scan the TypeScript file for gql (GraphQL Query) and will generate the appropriate TypeScript definition for the response data.

Download Unified Schema

The download step is a matter of using curl. I create a simple bash file:

curl -X GET \\
  http://retracted/allSchema.graphql \\
  -H 'accept: text/plain, */*' \\
  --silent \\
  --output allSchema.graphql

Query the GraphQL

The next step is to write the client code to query the data. There is many ways to do it, I'll demonstrate one but will not go in detail in this article.

async () => {
            try {
                const result: ApolloQueryResult<Proto.Query> = await this.client.query<Proto.Query>({
                    query: gql`
                        query Proto {
                            org(orgId: 0) {
                                name
                            }
                        }
                    `
                });
                if (result.data.org && result.data.org.name) {
                    this.setState({
                        longName: result.data.org.name
                    });
                }
            } catch (error) {
                console.error(error.networkError.bodyText);
            }
        }

At that point, your may wonder what is "Proto.Query" type and how come the result is strongly typed. The "Proto.Query" is the type that will be generated from the query specified in the client.query. The gql contains a query name which is translated into a TypeScript's namespace. It is important to name each query differently because of potential collisions. In my case, the org entity has only a single field, but later I might request more which will generate another org. A good way is to not call it "Proto" but something more relevant. For example, "Financial" or "Inventory" depending on the usage of the entity. Still, how do we generate the object.

Generate the TypeScript type

The generation is done by using a library named graphql**-code-generator** which scan the TypeScript file for gql and uses the unified schema (or could work by directly connecting to the GraphQL) and output in a specified folder the type. It means that the first time you are writing the gql, you should not strongly type the ApolloQueryResult, then will have access to the type. Then, every change in the type will mend the existing type which is a great experience. For example, removing in the gql a field that is being used in TypeScript will make the code not compile. The "graphql-code-generator" library has a bin that you can use in your package.json to read the codegen.yml file with your custom configuration. I added two scripts. One that analyzes the TypeScript files and generates and another one that I use while developing who constantly check in the background to generate types.

"generatetypes": "gql-gen --config graphql/codegen.yml",
"generatetypes:watch": "gql-gen --config graphql/codegen.yml --watch"

The codegen.yml has the schema and document configuration. At first, I was confusing about these two terms. The documentation is dry on the differentiation. The schema configuration is about where to get the GraphQL schema, this is the downloaded unified GraphQL schema from the server. The document is which TypeScript file to analyze (could be JavaScript file as well).

schema: "./graphql/**/*.graphql"
documents: "./src/**/*.+(tsx|ts)"
overwrite: true
generates:
  ./src/autogenerated/octoolstypes.ts:
    config: {}
    plugins:
      - "typescript-common"
      - "typescript-client"
require: ['ts-node/register']

It uses few plugins, hence you need to get the NPM packages accordingly.

"graphql-code-generator": "0.16.0",
"graphql-codegen-add": "0.16.0",
"graphql-codegen-typescript-client": "0.16.0",
"graphql-codegen-typescript-common": "0.16.0",
"graphql-codegen-typescript-server": "0.16.0"

The graphqlcodegen will check and generate the code. Unfortunately, it is slow. Even on a small code base, the process can take 10-15 seconds. Nonetheless, you have your TypeScript type generated for you and it will adapt with the backend (GraphQL server) changes.

Conclusion

Having the type generating automatically from the backend is fascinating and is efficient. If your business domain is rich in quantity of entities (interfaces) and if many of them have several fields and complex interconnection than having the type generated instead of typing it manually is a relief.

Related GraphQL Articles