Skip to content
Codeloom
GraphQL

GraphQL Code Generation with TypeScript

Automate type-safe GraphQL development using graphql-codegen to generate TypeScript types, hooks, and resolvers from your schema.

·6 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How graphql-codegen generates TypeScript types from your schema
  • How to generate typed React hooks for queries and mutations
  • How to generate server-side resolver type signatures

Prerequisites

  • Basic GraphQL knowledge
  • TypeScript fundamentals

The Problem: Manual Type Duplication

Without code generation, you define types twice: once in your GraphQL schema and again in TypeScript. Every time the schema changes, you manually update your TypeScript interfaces. This leads to drift, runtime errors, and wasted time.

# Schema defines User
type User {
  id: ID!
  name: String!
  email: String!
  role: Role!
}
// Manually maintained TypeScript type -- might be out of sync
interface User {
  id: string;
  name: string;
  email: string;
  role: 'ADMIN' | 'USER'; // Did someone add EDITOR to the enum?
}

GraphQL Code Generator eliminates this duplication by reading your schema and operations to produce TypeScript types automatically.

Setting Up graphql-codegen

Install the core packages:

npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript-resolvers

Create a codegen.ts configuration file:

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: './src/schema/**/*.graphql',
  documents: './src/**/*.{ts,tsx}',
  generates: {
    './src/generated/graphql.ts': {
      plugins: [
        'typescript',
        'typescript-operations',
      ],
    },
  },
};

export default config;

Add scripts to your package.json:

{
  "scripts": {
    "codegen": "graphql-codegen",
    "codegen:watch": "graphql-codegen --watch"
  }
}

Run the generator:

npm run codegen

What Gets Generated

The typescript plugin generates types from your schema:

// Generated from schema
export type Role = 'ADMIN' | 'EDITOR' | 'USER';

export type User = {
  __typename?: 'User';
  id: string;
  name: string;
  email: string;
  role: Role;
};

export type Post = {
  __typename?: 'Post';
  id: string;
  title: string;
  content: string;
  author: User;
  createdAt: string;
};

export type Query = {
  __typename?: 'Query';
  users: Array<User>;
  post?: Post | null;
};

The typescript-operations plugin generates types from your query and mutation documents:

// Your operation
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

// Generated types
export type GetUserQueryVariables = {
  id: string;
};

export type GetUserQuery = {
  __typename?: 'Query';
  user?: {
    __typename?: 'User';
    id: string;
    name: string;
    email: string;
  } | null;
};

Notice the generated query type only includes the fields you actually selected, not the entire User type. This matches exactly what the API returns.

Generating React Hooks

For React projects with Apollo Client, add the React Apollo plugin:

npm install -D @graphql-codegen/typescript-react-apollo

Update your config:

const config: CodegenConfig = {
  schema: './src/schema/**/*.graphql',
  documents: './src/**/*.{ts,tsx}',
  generates: {
    './src/generated/graphql.ts': {
      plugins: [
        'typescript',
        'typescript-operations',
        'typescript-react-apollo',
      ],
      config: {
        withHooks: true,
        withComponent: false,
      },
    },
  },
};

This generates typed hooks:

// Generated hook
export function useGetUserQuery(
  baseOptions: Apollo.QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
  return Apollo.useQuery<GetUserQuery, GetUserQueryVariables>(
    GetUserDocument,
    baseOptions
  );
}

Use the generated hook in your components:

import { useGetUserQuery } from '../generated/graphql';

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useGetUserQuery({
    variables: { id: userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  // data.user is fully typed -- autocomplete works
  return (
    <div>
      <h1>{data?.user?.name}</h1>
      <p>{data?.user?.email}</p>
    </div>
  );
}

Generating Resolver Types

For the server side, generate resolver type signatures:

npm install -D @graphql-codegen/typescript-resolvers
const config: CodegenConfig = {
  schema: './src/schema/**/*.graphql',
  generates: {
    './src/generated/resolvers-types.ts': {
      plugins: ['typescript', 'typescript-resolvers'],
      config: {
        contextType: '../context#GraphQLContext',
        mappers: {
          User: '../models/User#UserModel',
          Post: '../models/Post#PostModel',
        },
      },
    },
  },
};

The contextType option tells codegen about your context shape. The mappers option maps GraphQL types to your database models, handling the difference between what your database returns and what GraphQL exposes.

Use the generated types in your resolvers:

import { Resolvers } from '../generated/resolvers-types';

export const resolvers: Resolvers = {
  Query: {
    users: async (_, __, context) => {
      // context is typed as GraphQLContext
      return context.db.users.findAll();
    },
    post: async (_, { id }, context) => {
      // id is typed as string, return type is checked
      return context.db.posts.findById(id);
    },
  },
  User: {
    // TypeScript knows the parent is UserModel, not the GraphQL User type
    posts: async (parent, _, context) => {
      return context.db.posts.findByAuthorId(parent.id);
    },
  },
};

Document Presets: Near-Operation Types

The near-operation-file preset generates types next to the files that use them instead of one large generated file:

const config: CodegenConfig = {
  schema: './src/schema/**/*.graphql',
  documents: './src/**/*.{ts,tsx}',
  generates: {
    './src/': {
      preset: 'near-operation-file',
      presetConfig: {
        extension: '.generated.ts',
        baseTypesPath: 'generated/graphql.ts',
      },
      plugins: ['typescript-operations', 'typescript-react-apollo'],
    },
    './src/generated/graphql.ts': {
      plugins: ['typescript'],
    },
  },
};

This creates UserProfile.generated.ts next to UserProfile.tsx, making imports cleaner and keeping generated code co-located with its consumers.

Watch Mode and CI Integration

During development, run codegen in watch mode:

npm run codegen:watch

In CI, run codegen and check for changes to catch schema drift:

# GitHub Actions
- name: Generate types
  run: npm run codegen

- name: Check for uncommitted generated files
  run: |
    git diff --exit-code src/generated/
    if [ $? -ne 0 ]; then
      echo "Generated files are out of date. Run npm run codegen."
      exit 1
    fi

Client Preset (Modern Approach)

The newer client preset combines multiple plugins into a single streamlined setup:

npm install -D @graphql-codegen/client-preset
const config: CodegenConfig = {
  schema: './src/schema/**/*.graphql',
  documents: './src/**/*.{ts,tsx}',
  generates: {
    './src/gql/': {
      preset: 'client',
      config: {
        documentMode: 'string',
      },
    },
  },
};

This generates a graphql() function that provides type inference:

import { graphql } from '../gql';

const GET_USER = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`);

// TypeScript infers the result type from the query string
const { data } = useQuery(GET_USER, { variables: { id: '1' } });
// data.user.name is typed as string

Handling Custom Scalars

Map custom scalars to TypeScript types:

const config: CodegenConfig = {
  schema: './src/schema/**/*.graphql',
  generates: {
    './src/generated/graphql.ts': {
      plugins: ['typescript'],
      config: {
        scalars: {
          DateTime: 'string',
          JSON: 'Record<string, unknown>',
          Upload: 'File',
          PositiveInt: 'number',
        },
      },
    },
  },
};

Summary

GraphQL Code Generator eliminates type duplication between your schema and TypeScript code. Use the typescript plugin for schema types, typescript-operations for query and mutation types, typescript-react-apollo for generated hooks, and typescript-resolvers for server-side resolver signatures. Run codegen in watch mode during development and validate generated files in CI. The modern client preset provides the cleanest developer experience with automatic type inference from query strings.