GitHub GraphQL API and TypeScript

While I was working on a my-badges project, I needed to integrate with the GitHub GraphQL API. I wanted type safety, and at beginning, I was writing TypeScript types manually. But soon, it became a pain to maintain. I noticed that the GraphQL queries were almost identical to my TypeScript types. I wanted to generate the types from the queries. So I created Megaera, a GraphQL-to-TypeScript generator.

How to generate types from GraphQL queries?

Prerequisites:

  1. Node.js or Deno or Bun
  2. Megaera: npm install megaera
  3. GitHub GraphQL API Access: Obtain a personal access token with appropriate scopes.

# Step 1: Write a GraphQL Query

I decided what storing queries in separate .graphql files will be easier. First, for my tool to work with files. Second, my IDE has a great support for queries in a separate files (easier to debug and run queries).

query RepositoriesQuery($login: String!) {
  user(login: $login) {
    repositories(first: 10) {
      nodes {
        name
        description
        stargazers {
          totalCount
        }
        primaryLanguage {
          name
        }
      }
    }
  }
}

The query uses $login to fetch repositories for a specific user. Megaera generates also type for variables of the query.


# Step 2: Run Megaera

Run Megaera to generate TypeScript types:

npx megaera --schema=./schema.graphql ./repositories.graphql

Megaera generates a repositories.graphql.ts file with:

export const RepositoriesQuery = `
query RepositoriesQuery($login: String!) {
  user(login: $login) {
    repositories(first: 10) {
      nodes {
        name
        description
        stargazers {
          totalCount
        }
        primaryLanguage {
          name
        }
      }
    }
  }
}` as string & RepositoriesQuery;

export type RepositoriesQuery = (vars: { login: string }) => {
  user: {
    repositories: {
      nodes: Array<{
        name: string;
        description: string | null;
        stargazers: {
          totalCount: number;
        };
        primaryLanguage: {
          name: string | null;
        } | null;
      }>
    };
  };
};

One important thing to note is that the RepositoriesQuery type is attached to the query: &. This will allow to inherit types later in our code.


# Step 3: Use the Generated Types

import { Octokit } from '@octokit/core';
import { Query, Variables } from 'megaera';
import { RepositoriesQuery } from './repositories.graphql.ts';

const octokit = new Octokit({
  auth: process.env.GITHUB_TOKEN,
});

function query<T extends Query>(query: T, variables?: Variables<T>) {
  return octokit.graphql<ReturnType<T>>(query, variables);
}

void async function main() {
  const repositories = await query(RepositoriesQuery, { login: 'antonmedv' });
  console.log(repositories);
}()

The query() function will inherit types from the query! Even autocomplete for variables will work too! This is cool! 😎 We don't need to specify return type and variables types manually.


# Step 4: Fragments

When using fragments, Megaera generates separate types. Example:

fragment Repository on Repository {
  name
  stargazers {
    totalCount
  }
}

query RepositoriesQuery($login: String!) {
  user(login: $login) {
    repositories(first: 10) {
      nodes {
        ...Repository
      }
    }
  }
}

Generated TypeScript includes a Repository type usable independently or with RepositoriesQuery.

import { Repository, RepositoriesQuery } from './repositories.graphql.ts';

const resp = await query(RepositoriesQuery, { login });

const repositories: Repository[] = resp.user.repositories.nodes;

We can use fragment type independently in our code now. Checkout this example for fragment usage.


Difference with GraphQL Codegen

Main difference is that Megaera generates types for operations (queries and mutations), not for the whole schema. This allows you to generate types for only a part of the schema, e.g. only for a specific query. GraphQL Codegen generates types for the whole schema, which is not very useful for a large schema.

Another difference is that Megaera significantly easier to configure. Megaera requires no configuration at all. Megaera is just a CLI tool.

Conclusion

Megaera bridges TypeScript and GraphQL. I find it very useful for generating types, embedding queries, and ensuring type safety. It simplifies GraphQL workflows while maintaining scalability.

22
1672

Comments

No comments yet. You can be the first one!