DEV Community

Cover image for What's new in GraphQL-Codegen v2?
TheGuildBot for The Guild

Posted on • Updated on • Originally published at the-guild.dev

What's new in GraphQL-Codegen v2?

This article was published on Tuesday, August 3, 2021 by Dotan Simha @ The Guild Blog

GraphQL Code Generator is around for almost
5 years, and it's improving the developer experience of many developers (>3M downloads a month on
NPM!). Like all The Guild's projects, throughout all these years, the library continuously evolved
based on the experience and feedback from the community and our clients - every day codegen gets
better and better, so keep your feedback coming and keep your dependencies up to date :) But now,
it's time for a big release!

We recently decided to revisit and improve some parts of codegen, and we're happy to announce that
we're releasing a new version of the tool (v2)!

This release is all about improving the reliability and readability of the generated code, and
adding new features (and a new plugin/preset that might revolutionize how we generate code on our
frontends!).

Why v2?

Until now GraphQL Codegen has evolved, without the need for (major) breaking changes, but there were
a couple of things we wanted to do for a better and easier codegen experience that required some
small breaking changes.

We are currently working on adding ESM support for all The Guild's tools.

Unfortunately, in Codegen, we are not there yet
(you can track the progress here,
because we need to make sure that plugins, presets, schemas and documents are still being loaded
correctly. But it's coming soon!

But other tools, which we use as a dependency, like
graphql-tools (v8) already supports ESM, and dropped support for Node
10, so we are aligning to that.

We've updated to the latest version of graphql-tools (v8) and
graphql-config (v4) to get some upstream bug fixes.

Human-Readable Generated Types

A few years ago, we changed the output of codegen to use Pick in order to build the operation
types based on the schema types:

// Base types generated based on the schema, from `typescript` plugin:
type Query = {
  user: User
}

type User = {
  id: Scalars['ID']
  name: Scalars['String']
}

// Types generate based on operations, from `typescript-plugins`:

type MyQuery = {
  user: Pick<User, 'id' | 'name'>
}
Enter fullscreen mode Exit fullscreen mode

Some time after, we added preResolveTypes configuration flag as experimental, in order to generate
more readable types with primitive types resolved directly on the generate types:

type MyQuery = {
  user: {
    id: string
    name: string
  }
}
Enter fullscreen mode Exit fullscreen mode

We are happy to announce that v2 of typescript-operations now uses preResolveTypes: true by
default, so all generated types are more readable.

As a user of Codegen this is not a breaking change (aside that the generated code is slightly
different), and all your types are still fully compatible with v1 of the tool. You can set
preResolveTypes: false if you prefer to keep the old behavior.

Type Accuracy

In addition to more readable types, we also improved the accuracy of the generated types.

A few months ago, chrbala found
a bug in the typescript-operations plugin,
which was causing the generated types to be incorrect in cases where you use nested and complex
fragments.

This bug was caused by the fact that we were combining the fragment types using the & operator of
TypeScript, and this operator doesn't apply deep merging for nested sub-types. So when using
multiple fragments (MyFirstFragment & MySecondArgument), the nested fields are being overwritten,
instead of being merged.

n1ru4l picked that up and
fixed the bug and added support
for better handling of these kind of cases.

The new configuration flag (inlineFragmentTypes: inline) is now used by default, and generates
more accurate types, without introducing breaking changes!

Instead of combining the fragment types they are merged and inlined.

export type FeedQuery = { __typename?: 'Query' } & {
-  currentUser?: Maybe<{ __typename?: 'User' } & Pick<User, 'login'>>;
-  feed?: Maybe<Array<Maybe<{ __typename?: 'Entry' } & FeedEntryFragment>>>;
+  currentUser?: Maybe<{ __typename?: 'User'; login: string }>;
+  feed?: Maybe<
+    Array<
+      Maybe<{
+        __typename?: 'Entry';
+        id: number;
+        commentCount: number;
+        score: number;
+        createdAt: number;
+        repository: {
+          __typename?: 'Repository';
+          full_name: string;
+          html_url: string;
+          description?: Maybe<string>;
+          stargazers_count: number;
+          open_issues_count?: Maybe<number>;
+          owner?: Maybe<{ __typename?: 'User'; avatar_url: string }>;
+        };
+        vote: { __typename?: 'Vote'; vote_value: number };
+        postedBy: { __typename?: 'User'; html_url: string; login: string };
+      }>
+    >
+  >;
};
Enter fullscreen mode Exit fullscreen mode

No worries, fragment types are still exported separately!

All your types are still fully compatible with v1 of the tool. You can set
inlineFragmentTypes: combine if you still prefer to keep the old behavior.

Removal of Deprecations

We also used the need for a major version in order to remove a few deprecations from the codebase.

typescript-resolvers

In typescript-resolvers, we had a generated signature for IResolvers and IDirectiveResolvers
(which were deprecated 2 years ago), v2 of this plugin is removing these proxy types.

Also, the noSchemaStitching flag is now set to true by default, so the generated resolvers
signature is simpler and matches the needs of most projects.

If you are using Schema Stitching, you can set noSchemaStitching: false to keep the old
behavior.

typescript-compatibility

The typescript-compatibility plugin was created to make it easier to migrate from v0 to v1 of
codegen, and hasn't been actively developed for a few years.

With this release, we no longer support or maintain the typescript-compatibility plugin. If you
are still using it, please consider migrating to the new type format.

New TypeScript Plugin for Operations!

Some time ago we started advocating the
near-operation-file preset,
for generating GraphQL client code next to the .graphql operation file.

A month ago, Maël Nison reached out to us with
a new concept for matching your actual
GraphQL operation string and the generated TypeScript types. Without having to add clumsy import
statements for each GraphQL operation.

It allowed writing the operations in the following way:

import { gql } from './gql'

const { GetTweets, CreateTweet } = gql(`#graphql
  query GetTweets {
    Tweets {
      id
    }
  }

  mutation CreateTweet {
    CreateTweet(body: "Hello") {
      id
    }
  }
`)
Enter fullscreen mode Exit fullscreen mode

The README on his repository contained a
list of limitations. We
took that that feedback and fixed some upstream bugs within graphql-tools and improved
graphql-codegen according to that.

With a few adjustments, n1ru4l managed to
turn that amazing idea into a plugin and a preset
that extends the behavior of typescript-operation and TypedDocumentNode, and allow you to
generate TypeScript types that magically matches your GraphQL query strings (this also required a
few fixes in graphql-tools, so now the Loaders in graphql-tools are better!).

So What Does That Mean?

Today, GraphQL Codegen scans your codebase and looks for all the operations that are being used by
your components (or, from .graphql files), then it generates types and wraps TypedDocumentNode
for you. Those are either generated to a single file, from which you import from anywhere within the
app or next to the .graphql when using the near-operation-file-preset. Folder structure
(near-operation-file-preset)

Project
- src
  - UserQuery.graphql
  - UserQuer.graphql.tsx
Enter fullscreen mode Exit fullscreen mode

With this new plugin+preset, you can generate typings for your inline gql function usages, without
having to manually specify import statements for the documents. All you need to do is import your
gql function and run codegen in watch mode.

import { FragmentType, gql } from '@app/gql'

// TweetFragment is a fully typed document node
const TweetFragment = gql(/* GraphQL */ `
  fragment TweetFragment on Tweet {
    id
    body
  }
`)

const TweetsQueryWithFragment = gql(/* GraphQL */ `
  query TweetsWithFragmentQuery {
    Tweets {
      id
      ...TweetFragment
    }
  }
`)

const TweetComponent = (props: { tweet: FragmentType<typeof TweetFragment> }) => {
  return <Tweet>{props.tweet.body}</Tweet>
}
Enter fullscreen mode Exit fullscreen mode

That way we don't need to switch context between .graphql and our .ts(x) files and have less
files within our repository (compared to the near-operation-file preset)!

You can find
the complete documentation, examples and API reference in codegen website.

We also have an example PR for a migration from near-operation-file to the gql-tag-operations-preset.

We want to hear your thoughts on this way of using Codegen on the frontend.

Based on your feedback we might want to make this the recommended way of using Codegen on the
frontend in the future.

Huge thanks to Maël Nison, who conceptualized the foundation for
this preset over here. Please keep
pushing the boundaries!

What's Next?

Like we said at the beginning, we continuously keep improving Codegen, so this release is not the
end but just a start. Here are some sneak peeks on things we are currently working on:

ESM Support

ESM support is coming soon (and will probably result in another major version)

SDL Resolver Development Flow Improvements

We are experimenting with a better way to link typeDefs with the resolvers signature generated by
typescript-resolvers in a similar way to the new gql-tag-operations plugin. One of the goal of
this preset is to make sure that every resolver that should be defined and implemented is actually
implemented.

import { typeDefs } from 'tsgql'

const sdl = typeDefs(/* GraphQL */ `
  type Query {
    foo: String!
  }
`)

export const resolvers: typeof sdl = {
  Query: {
    // If this resolver would not be defined it will raise a TypeScript error.
    foo: () => 'test'
  }
}
Enter fullscreen mode Exit fullscreen mode

You can find more info here

Fragment Type Masking

Today the Relay framework development flow allows hiding (masking) properties from operation results
objects that are added via fragments. Those properties are only accessible from within the component
that consumes the fragment:

import { FragmentProperty, gql } from '@app/gql'

// TweetFragment is a fully typed document node
const TweetFragment = gql(/* GraphQL */ `
  fragment TweetFragment on Tweet {
    id
    body
  }
`)

const TweetsQueryWithFragment = gql(/* GraphQL */ `
  query TweetsWithFragmentQuery {
    Tweets {
      id
      ...TweetFragment
    }
  }
`)

const AppComponent = () => {
  const { data } = useQuery({ query: TweetsQueryWithFragment })

  // accessing this here is a TypeScript error.
  data.Tweets[0].body

  return (
    <>
      {data.Tweets.map(tweet => (
        <TweetComponent key={tweet.id} tweet={tweet} />
      ))}
    </>
  )
}

const TweetComponent = (props: { tweet: FragmentProperty<typeof TweetFragment> }) => {
  // accessing props.tweet.body here is legit.
  return <Tweet>{props.tweet.body}</Tweet>
}
Enter fullscreen mode Exit fullscreen mode

This allows building scalable components as each component only receives the data it should have
access to and furthermore components higher up in the tree can not access data it did not explicitly
request. Removing a component will thus not result in surprising behaviors.

What Else Is Missing?

Do you think something else is missing or could be improved? Reach out to us via the chat on this
page, a GitHub discussion or on
the GraphQL Discord.

Top comments (0)