DEV Community

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

Posted on • Originally published at the-guild.dev

What's new in GraphQL-Codegen v2?

This article was published on 2021-08-03 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 of 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 { gql, FragmentType } 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 wanna 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 { gql, FragmentProperty } 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.

Discussion (0)