Arpad Borsos Blog Resume

GraphQL Code generators

Dreaming up the next generation of tools

— 5 min

Prompted by a recent meetup with former work colleagues, I had another short look at relay as an alternative to apollo which we currently use and with which we are not quite that happy.

Both libraries have support for typescript code generation, but relay puts it more in the center.

All in all, the problems, or rather improvements and dreams I have come from the fact that strict typing and code generation was added as an afterthought. You can use both libraries a) with untyped JS and b) without code generation at all.

Apart from the very obvious problem that the apollo-cli itself is horrible, both from a usage as well as a project viewpoint (I mean it pulls in yarn as a dependency transitively, wtf?), I see the following problems or lack of features in both relay and apollo when it comes to typescript support and codegen.

There is a lot of footguns

Lets create an example slightly changed from the relay docs.

import { ExampleQuery } from "__generated__/ExampleQuery.graphql"

const QUERY = graphql`
    query ExampleQuery($artistID: ID!) {
      artist(id: $artistID) {
        name
      }
    }
  `

<QueryRenderer<ExampleQuery>
  query={QUERY}
  variables={{ artistID: 'banksy' }}
/>

As this example shows, code generation and typescript support revolves around importing a generated type definition and manually providing it as optional type parameter to the QueryRenderer component. This perfectly demonstrates the two points I made before. Remove the import and the type parameter, and the code becomes valid untyped JS.

However, there are a couple of obvious footguns even with this very simple example.

The example from the apollo docs is even worse, since the results and the variables are provided as two separate type parameters. So there is even more opportunity to mess up.

Digression: Component-based API

This might just be a personal preference, but I consider Components with a render-prop API a horrible design pattern in general. Especially for usage with graphql, I consider this to be extremely unergonomic and tedious.

Using react hooks is a hugely better alternative. But true, both relay and apollo are older than the hooks API, so I will let this one slide.

Types are mis-used

One problem I see with the generated types in our codebase is that developers are actually mis-using them. This is not really specific to the way graphql libraries work, but a problem I see in general when it comes to typed react code.

One very good design pattern in react is to split code into presentational components, that just display some data, and container components, which might include some business logic and data fetching logic. In my opinion, a presentational component should itself declare what kind of props it wants to receive. And the type checker makes sure that the container components provides valid props.

Well in reality, developers will just happily import graphql-generated types for use in their presentational components, which I personally consider to be a red flag and a mis-use of those types.

Example:

import { VenueLogoWrapperVenueQuery_venue_logo } from "../container/genTypes/VenueLogoWrapperVenueQuery";

interface LogoWrapperProps {
  logo?: VenueLogoWrapperVenueQuery_venue_logo | null;
}

This surely gets a huge facepalm from me.

It does not support custom scalars

GraphQL itself has a super simple concept. You just provide a query string and optional variables and you get back some json data. You don’t even need a big-ass library, all you need is fetch. But this also means you are limited to datatypes representable as json. Which means you have null instead of undefined, and you don’t have rich types such as Date. There is even an old feature request for apollo to generate undefined instead of null, but I think it unlikely for that ever to happen.

For all of the sophistication these libraries have, they are actually quite dumb in this regard. All they do is pass on the result json unaltered.

A dream takes form

With all these issues in mind, lets brainstorm and dream up an ideal API.

What if, a next generation graphql code generator generated some code like this:

import { getVenue, getMe, requestPasswordReset } from "./some-generated-file";

const venue = await getVenue(conn, { id: "…" });
// venue = { name: string, logo?: Image, … }

const me = await getMe(conn);
// me = { lastLogin: Date, … }

const result = await requestPasswordReset(conn, { email: "…" });
// …

So my proposed next-generation graphql code generator will generate ready to use, strongly typed async functions, which you can just call with a connection (essentially a parameterized fetch, apollo-client, or whatever), and some strongly typed variables.

These functions will embed the query string itself, and will do type conversions for both input and result types, in the example above the Image and Date scalars, which would be automatically deserialized to typescript classes, or enums, or whatever.

This dreamed-up example was not react-specific, but it would be super easy to also create ready to use hooks which would consume the connection via context and thus be even easier to use.

Essentially, it will solve the following problems, plus give even more advantages:

Inspiring other projects

Now that I have brainstormed the graphql code generator of my dreams, it also got me thinking about another one of my projects. Another code generation success story is intl-codegen, which similarly generates ready to use functions out of MessageFormat strings, with the goal of both avoiding to ship a MessageFormat parser to runtime as well as to provide not quite so strong typing.

I could imagine that instead of having a single Localized component, it could actually generate a separate component for each message.

// now:
<Localized id="some-message-id" params={{ name: "some name" }} />
// future?:
<SomeMessageId name="some name" />

I’m not quite convinced yet if this would be a worthwile change, but it certainly would make it impossible to mis-use the current implementation by using a dynamic id, which circumvents typing.

In general, I have neglected intl-codegen quite a bit recently, but I do have a lot of ambitious ideas about its future. But those ambitious ideas are also hard to implement so it will take quite some more time to think about those.