Shopify / graphql-codegen

A codegen preset for generating TypeScript types from GraphQL queries without AST wrappers.
MIT License
22 stars 0 forks source link

Shopify GraphQL Codegen

A codegen plugin and preset for generating TypeScript types from GraphQL queries in a d.ts file. It does not require any function wrapper and adds no runtime overhead (0 bytes to the bundle).

This package was originally extracted from @shopify/hydrogen-codegen to be agnostic of Hydrogen and to be used with any GraphQL client.

The GraphQL client must use TypeScript interfaces that are extended in the generated d.ts file. This package also exports utility types to create GraphQL clients that comply to these interfaces.

// `shop` is inferred as {name: string, description: string}
const {shop} = await clientQuery(`#graphql
  query layout {
    shop {
      name
      description
    }
  }
`);

Benefits

Caveats

Usage

With GraphQL CLI

You can use the following example configuration with GraphQL CLI:

// <root>/codegen.ts

import type {CodegenConfig} from '@graphql-codegen/cli';
import {pluckConfig, preset} from '@shopify/graphql-codegen';

export default {
  overwrite: true,
  pluckConfig,
  generates: {
    'my-api.generated.d.ts': {
      preset,
      schema: './path/to/my-api-schema.json',
      documents: ['./src/graphql/my-api/*.{ts,tsx,js,jsx}'],
      presetConfig: {
        // Generate the line `import type * as MyAPI from 'path/to/my-api-types';`.
        // If you don't have generated types for your API beforehand,
        // omit this parameter to generate the types inline.
        importTypes: {
          namespace: 'MyAPI',
          from: 'path/to/my-api-types',
        },

        // Skip the __typename property from generated types:
        skipTypenameInOperations: true,

        // Add a custom interface extension to connect
        // the generated types to the GraphQL client:
        interfaceExtension: ({queryType, mutationType}) => `
          declare module 'my-api-client' {
            interface MyAPIQueries extends ${queryType} {}
            interface MyAPIMutations extends ${mutationType} {}
          }
        `,
      },
    },
  },
} as CodegenConfig;

[!TIP] To use enums instead of types, change the generated file extension to .ts instead of .d.ts.

Then, include queries in your app that match the given schema and documents paths. For example, for a query layout like the one in the example above, the generated d.ts file will look like this:

// my-api.generated.d.ts
import type * as MyAPI from 'path/to/my-api-types';

export type LayoutQueryVariables = MyAPI.Exact<{[key: string]: never}>;

export type LayoutQuery = {
  shop: Pick<MyAPI.Shop, 'name' | 'description'>;
};

interface GeneratedQueryTypes {
  '#graphql\n  query layout {\n    shop {\n      name\n      description\n    }\n  }\n': {
    return: LayoutQuery;
    variables: LayoutQueryVariables;
  };
}

interface GeneratedMutationTypes {}

declare module 'my-api-client' {
  interface MyAPIQueries extends GeneratedQueryTypes {}
  interface MyAPIMutations extends GeneratedMutationTypes {}
}

Therefore, when passing the query to the GraphQL client, TypeScript will infer the type of the response and the variables parameter.

Making GraphQL clients

To make a GraphQL client that complies with the generated types, you can use the following utility types:

// my-api-client.ts
import type {
  ClientReturn,
  ClientVariablesInRestParams,
} from '@shopify/graphql-codegen';

// Empty interface to be extended with the generated queries and mutations in user projects.
export interface MyAPIQueries {}

export async function clientQuery<
  OverrideReturnType extends any = never,
  RawGqlString extends string = string,
>(
  query: RawGqlString,
  ...params: ClientVariablesInRestParams<MyAPIQueries, RawGqlString>
): Promise<ClientReturn<MyAPIQueries, RawGqlString, OverrideReturnType>> {
  // The 'params' and 'params.variables' are optional
  // if the query has no variables, required otherwise.
  const {variables} = params[0] ?? {};

  // Client implementation (this could be forwarding the query to Apollo, urql, useQuery, etc.)
  const response = await fetch('https://my-api.com/graphql', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({query, variables}),
  });

  return response.json();
}

This allows to use the clientQuery function with any query string and TypeScript will infer the type of the response and the variables parameter. It's also possible to override the return type of the query by passing a type argument to clientQuery:

// Inferred type from the query:
data = await clientQuery('query layout {shop {name}}');

// Overwritten return type:
data = await clientQuery<{shop: {name: string}}>('...');

The type utilities accept other optional type params:

ClientVariablesInRestParams<
  // Interface extended with queries in the user project
  MyAPIQueries,
  // The raw query string that comes from the user
  RawGqlString,
  // -- Optional:
  // Additional properties to the params object.
  // E.g. `{cache: Cache; debug?: boolean}`
  AdditionalParamOptions,
  // Query variables that the client automatically injects.
  // E.g. `'locale' | 'currency'`
  AutoInjectedVariables
>;

To see a full example of a GraphQL client using these types, check the Hydrogen's Storefront client.

Wrapping this preset

This preset can be wrapped in a package that generates types for a specific API and adds default values to the preset. For example, an all-in-one solution for an "Example API" would be a package that provides:

// example-api/preset.ts

import {preset as internalPreset} from '@shopify/graphql-codegen';
export {pluckConfig} from '@shopify/graphql-codegen';

// Export a custom preset that adds default values to @shopify/graphql-codegen:
export const preset = {
  buildGeneratesSection: (options) => {
    return internalPreset.buildGeneratesSection({
      ...options,
      // Known schema path from the example-api package:
      schema: 'example-api/schema.json'
      presetConfig: {
        importTypes: {
          namespace: 'ExampleAPI',
          // Known types path from the example-api package:
          from: 'example-api/types',
        },
        interfaceExtension: ({queryType, mutationType}) => `
          declare module 'example-api/client' {
            interface AdminQueries extends ${queryType} {}
            interface AdminMutations extends ${mutationType} {}
          }`,
      },
    });
  },
};

When the user imports this new preset, they don't need to specify the schema and types paths:

// <root>/codegen.ts

// This uses @shopify/graphql-codegen internally:
import {preset, pluckConfig} from 'example-api/preset';

export default {
  overwrite: true,
  pluckConfig,
  generates: {
    'example-api.generated.d.ts': {
      preset,
      documents: ['**/*.ts'],
    },
  },
};