DevToolBoxGRATIS
Blogg

GraphQL til TypeScript: Komplett Guide til Kodegenerering med @graphql-codegen

11 min lesningby DevToolBox

TL;DR

Generate TypeScript types from GraphQL schemas and queries using @graphql-codegen/cli. Configure codegen.ts, run graphql-codegen --watch during development, and get fully typed queries, mutations, and fragments automatically. Try our free GraphQL to TypeScript tool →

Key Takeaways

  • @graphql-codegen/cli is the standard tool for GraphQL TypeScript code generation.
  • Add a generates config in codegen.ts pointing to your schema and operations.
  • TypedDocumentNode ensures TypeScript knows exactly what data each query returns.
  • All GraphQL types are nullable by default — use ! in your schema for non-null fields.
  • Custom scalars (DateTime, UUID) need TypeScript type mappings in codegen config.
  • Fragment types let you reuse partial type definitions across multiple components.
  • Run codegen --watch in dev and add codegen to CI to keep types in sync.

Why Generate TypeScript from GraphQL?

GraphQL gives you a strongly typed schema on the server side. Without code generation, you must manually write TypeScript interfaces that mirror every type, field, and operation in that schema. This creates a dual source of truth: if the server changes a field from String to Int, your TypeScript types stay wrong until someone manually updates them.

Code generation eliminates this problem. Tools like @graphql-codegen/cli read the actual schema and generate TypeScript types automatically. When the schema changes, you rerun the generator and TypeScript immediately flags any broken usages in your codebase. The benefits are significant:

Type Safety vs Manual Maintenance

ApproachType SafetyMaintenanceSchema Drift
Manual TypeScript interfacesPartialHigh — manual updatesSilently wrong
graphql-codegen (typescript)FullZero — auto-generatedBuild error on drift
graphql-codegen + TypedDocumentNodeFull + variablesZero — auto-generatedBuild error on drift
No types (any)NoneNoneRuntime errors only

Client-server synchronization is the most valuable benefit. When your frontend team writes a query that selects fields your backend no longer exposes, the TypeScript compiler catches it before the code reaches production. This is especially critical in large teams where frontend and backend evolve independently.

You can also try the DevToolBox GraphQL to TypeScript tool at https://viadreams.cc/en/tools/graphql-to-typescript for quick, browser-based schema-to-type conversion without installing anything locally.

Setting Up @graphql-codegen/cli

The core package is @graphql-codegen/cli. You always need it alongside two plugins: @graphql-codegen/typescript (base schema types) and @graphql-codegen/typescript-operations (types for your queries and mutations).

# Install CLI and core plugins
npm install -D \
  @graphql-codegen/cli \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typed-document-node

# Optional: typed hooks for Apollo Client
npm install -D @graphql-codegen/typescript-react-apollo

# Optional: typed hooks for React Query / TanStack Query
npm install -D @graphql-codegen/typescript-react-query

Add a script to package.json so you can run codegen with a short command:

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

codegen.ts Configuration

Create a codegen.ts file at the root of your project. The TypeScript config format gives you full IDE autocompletion and type checking for the configuration itself.

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

const config: CodegenConfig = {
  schema: 'https://your-api.com/graphql',  // or './schema.graphql'
  documents: ['src/**/*.graphql', 'src/**/*.tsx'],
  generates: {
    // 1. Base schema types
    'src/generated/graphql.ts': {
      plugins: ['typescript'],
      config: {
        scalars: {
          DateTime: 'string',
          UUID: 'string',
          JSON: 'Record<string, unknown>',
        },
        avoidOptionals: false,
        maybeValue: 'T | null',
      },
    },
    // 2. Operation types (queries, mutations, fragments)
    'src/generated/operations.ts': {
      plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
      config: {
        scalars: {
          DateTime: 'string',
          UUID: 'string',
        },
      },
    },
  },
  hooks: {
    afterAllFileWrite: ['prettier --write'],
  },
};

export default config;

The schema field accepts a GraphQL endpoint URL (codegen will introspect it), a path to a .graphql schema file, a glob pattern matching multiple schema files, or an introspection JSON. The documents field specifies where your operation files (queries, mutations, fragments) live.

TypedDocumentNode for Typed Queries

The typed-document-node plugin is the key that connects your GraphQL operations to TypeScript. Instead of an untyped DocumentNode, it generates a TypedDocumentNode<TResult, TVariables> that carries type information directly in the document object.

Write your operation in a .graphql file:

# src/queries/GetUser.graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    createdAt
    role
  }
}

After running npm run codegen, the generator produces:

// src/generated/operations.ts (auto-generated, do not edit)
export type GetUserQuery = {
  user: {
    id: string;
    name: string;
    email: string;
    createdAt: string;        // DateTime scalar → mapped to string
    role: UserRole;           // enum type
  } | null;
};

export type GetUserQueryVariables = {
  id: string;
};

export const GetUserDocument: TypedDocumentNode<GetUserQuery, GetUserQueryVariables> = {
  kind: 'Document',
  definitions: [/* AST nodes */],
};

Now GetUserDocument is a fully typed GraphQL document. Pass it to any compatible client library and TypeScript will automatically infer the query result and variables types.

React Query Integration with Generated Types

TanStack Query (React Query) does not have a built-in GraphQL client, but you can use it with a custom fetcher and TypedDocumentNode for full type safety. This approach is framework-agnostic and works with any HTTP client.

// lib/graphql-fetcher.ts
import { TypedDocumentNode } from '@graphql-typed-document-node/core';

export async function fetchGraphQL<TResult, TVariables>(
  document: TypedDocumentNode<TResult, TVariables>,
  variables: TVariables,
): Promise<TResult> {
  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query: document, variables }),
  });
  const json = await response.json();
  if (json.errors) throw new Error(json.errors[0].message);
  return json.data;
}

// components/UserProfile.tsx
import { useQuery } from '@tanstack/react-query';
import { GetUserDocument } from '../generated/operations';
import { fetchGraphQL } from '../lib/graphql-fetcher';

export function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchGraphQL(GetUserDocument, { id: userId }),
    // data is automatically typed as GetUserQuery
  });

  if (isLoading) return <div>Loading...</div>;

  // TypeScript knows data.user.name is string | null
  return <h1>{data?.user?.name}</h1>;
}

Alternatively, use the @graphql-codegen/typescript-react-query plugin to generate hooks directly. Configure it in codegen.ts:

// codegen.ts — add to generates section
'src/generated/react-query-hooks.ts': {
  plugins: ['typescript', 'typescript-operations', 'typescript-react-query'],
  config: {
    fetcher: {
      func: '../lib/graphql-fetcher#fetchGraphQL',
      isReactHook: false,
    },
  },
},

This produces a useGetUserQuery(variables) hook with fully typed return values and variable parameters.

Apollo Client with Generated Types

Apollo Client has native support for TypedDocumentNode. Pass the generated document to useQuery and TypeScript automatically infers the result and variables types:

import { useQuery } from '@apollo/client';
import { GetUserDocument } from '../generated/operations';

function UserProfile({ userId }: { userId: string }) {
  // No generic annotation needed — TypeScript infers from GetUserDocument
  const { data, loading, error } = useQuery(GetUserDocument, {
    variables: { id: userId },
  });

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

  return (
    <div>
      <h1>{data?.user?.name}</h1>
      <p>{data?.user?.email}</p>
    </div>
  );
}

For typed mutations with Apollo, the same pattern applies:

import { useMutation } from '@apollo/client';
import { UpdateUserDocument } from '../generated/operations';

function UpdateUserForm() {
  // TypeScript enforces correct variable shape
  const [updateUser, { loading }] = useMutation(UpdateUserDocument);

  const handleSubmit = async (data: { id: string; name: string }) => {
    await updateUser({
      variables: { id: data.id, input: { name: data.name } },
    });
  };

  return (/* form JSX */);
}

You can also use the @graphql-codegen/typescript-react-apollo plugin to generate useGetUserQuery and useUpdateUserMutation hooks directly, with inline types ready to use.

urql with Generated Types

urql also supports TypedDocumentNode natively. The useQuery hook from urql infers types from the document you pass:

import { useQuery } from 'urql';
import { GetUserDocument } from '../generated/operations';

function UserProfile({ userId }: { userId: string }) {
  const [{ data, fetching, error }] = useQuery({
    query: GetUserDocument,
    variables: { id: userId },
    // TypeScript infers: data is GetUserQuery | undefined
  });

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

  return <h1>{data?.user?.name}</h1>;
}

For urql, you can also use the @graphql-codegen/typescript-urql plugin to generate typed hooks directly. All three major GraphQL client libraries (Apollo, urql, React Query) work seamlessly with the TypedDocumentNode pattern.

Generating Only Types vs Full Client Code

You do not always need to generate hooks or client utilities. Sometimes you only need the TypeScript types that mirror your schema — for example, when you are building a server-side resolver in Node.js, or when you want to use a low-level fetch with manual type assertions.

Use only the typescript plugin for schema types:

// codegen.ts — types only
generates: {
  'src/generated/schema-types.ts': {
    plugins: ['typescript'],
    // This generates interfaces for every type in the schema
    // but does NOT generate operation-specific types or hooks
  },
}

For server-side resolvers (e.g., in a Node.js GraphQL server), use @graphql-codegen/typescript-resolvers:

'src/generated/resolvers.ts': {
  plugins: ['typescript', 'typescript-resolvers'],
  config: {
    contextType: '../context#MyContext',
  },
}
PluginUse CaseOutput
typescriptSchema types onlyTypeScript interfaces for all schema types
typescript-operationsClient query typesTypes for each query/mutation/fragment
typed-document-nodeTypedDocumentNodeTyped AST document with generics
typescript-react-apolloApollo Client hooksuseXxxQuery / useXxxMutation hooks
typescript-react-queryReact Query hooksTyped useQuery wrappers per operation
typescript-resolversServer resolversTyped resolver function signatures

Custom Scalar Types (DateTime, UUID, JSON)

GraphQL schemas frequently define custom scalars for domain-specific values. Without explicit mappings, codegen falls back to any, which defeats the purpose of type safety. Always map your scalars explicitly in codegen.ts:

// codegen.ts — scalar mappings
config: {
  scalars: {
    // ISO 8601 string from server → string in TypeScript
    DateTime: 'string',

    // UUIDs are strings
    UUID: 'string',

    // Opaque JSON blobs
    JSON: 'Record<string, unknown>',

    // If your app parses dates client-side
    Date: 'Date',

    // Money amounts (avoid float precision issues)
    Decimal: 'string',

    // Arbitrary binary data (e.g., file uploads)
    Upload: 'File',
  },
}

For DateTime scalars, a common pattern is to map them to string in the generated types and then parse them to Date objects in your UI layer using a utility like date-fns or dayjs. This keeps the generated types serialization-safe while giving you rich date manipulation in components.

Fragment Types

GraphQL fragments let you define reusable field selections. Code generation creates corresponding TypeScript types for each fragment, enabling you to share partial type definitions across components without duplication.

# src/fragments/UserFields.graphql
fragment UserFields on User {
  id
  name
  avatarUrl
}

# src/queries/GetUsers.graphql
query GetUsers {
  users {
    ...UserFields
    email
  }
}

Codegen generates a UserFieldsFragment type:

// Generated
export type UserFieldsFragment = {
  id: string;
  name: string;
  avatarUrl: string | null;
};

// Use as prop type in components
interface UserAvatarProps {
  user: UserFieldsFragment;
}

function UserAvatar({ user }: UserAvatarProps) {
  return <img src={user.avatarUrl ?? '/default-avatar.png'} alt={user.name} />;
}

Fragment types are especially useful for component-level data requirements. Each component declares the fragment it needs, and the parent query composes those fragments. This is known as the Colocation pattern and is a best practice with Apollo Client and Relay.

Input Types and Mutations

GraphQL mutations take input types as variables. Codegen generates TypeScript types for both the mutation result and the input variables. This means your form data, API calls, and state management are all type-checked against the schema.

# src/mutations/CreatePost.graphql
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    publishedAt
    author {
      id
      name
    }
  }
}
// Generated types
export type CreatePostInput = {
  title: string;
  body: string;
  tags?: string[] | null;
  publishAt?: string | null;  // DateTime scalar
};

export type CreatePostMutation = {
  createPost: {
    id: string;
    title: string;
    publishedAt: string | null;
    author: {
      id: string;
      name: string;
    };
  };
};

export type CreatePostMutationVariables = {
  input: CreatePostInput;
};
// Usage with Apollo
import { useMutation } from '@apollo/client';
import { CreatePostDocument } from '../generated/operations';

function CreatePostForm() {
  const [createPost, { loading, error }] = useMutation(CreatePostDocument);

  const handleSubmit = async (formData: CreatePostInput) => {
    const { data } = await createPost({
      variables: { input: formData },
      // TypeScript validates formData matches CreatePostInput
    });
    console.log('Created:', data?.createPost.id);
  };

  // ... form JSX
}

Watch Mode and CI Integration

Running codegen manually is error-prone. Use watch mode during development and integrate codegen into your CI pipeline to ensure types are always up to date.

Watch Mode in Development

# Start watch mode alongside your dev server
npx graphql-codegen --watch

# Or in package.json with concurrently:
{
  "scripts": {
    "dev": "concurrently \"next dev\" \"npm run codegen:watch\"",
    "codegen:watch": "graphql-codegen --watch"
  }
}

Watch mode monitors your documents glob and the schema source. When either changes, it regenerates the types automatically. No manual rerun needed.

CI Pipeline Integration

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

- name: Check for uncommitted changes
  run: |
    git diff --exit-code src/generated/
    # Fails if generated files were not committed

- name: TypeScript type check
  run: npx tsc --noEmit --skipLibCheck

The CI check pattern above ensures that:

  1. Generated types are always committed alongside the operations that produce them.
  2. Any schema change that breaks existing queries is caught as a TypeScript error in CI.
  3. Pull requests cannot merge with stale generated types.

Pre-commit Hook with Husky

# .husky/pre-commit
#!/bin/sh
npm run codegen
git add src/generated/

This hook regenerates types before each commit and stages the updated files automatically, so generated types are always in sync with operations in every commit.

Tools for GraphQL to TypeScript Conversion

Several tools can help you convert GraphQL schemas and operations to TypeScript types:

@graphql-codegen/cli

The industry-standard CLI tool. Supports all major GraphQL clients, produces TypedDocumentNode, resolver types, and more. Highly configurable via plugins. Best for production projects with ongoing schema evolution.

DevToolBox GraphQL to TypeScript

For quick, one-off conversions without installing anything, the DevToolBox GraphQL to TypeScript tool converts GraphQL SDL (Schema Definition Language) to TypeScript interfaces directly in your browser. Paste a schema fragment and get typed interfaces instantly. Useful for exploring a schema or learning what the generated types look like before setting up the full codegen pipeline.

Other Tools

ToolTypeBest For
@graphql-codegen/cliCLI / npmFull production pipeline
DevToolBox GraphQL→TSOnline browser toolQuick schema exploration
graphql-zeusCLI / npmFully-typed client generation
gqtyCLI / npmAuto-generating reactive queries
Relay compilerCLI / npmFacebook Relay framework

Common Pitfalls

Nullable Fields Everywhere

GraphQL fields are nullable by default. In a schema like type User { name: String }, the name field is String | null in TypeScript. Many developers are surprised by the volume of optional chaining required. The solution is to mark fields non-null in the schema with ! whenever the server guarantees a value: name: String!. Do not work around nullability by disabling strictNullChecks — that eliminates all the safety benefits.

__typename Conflicts

By default, codegen adds a __typename: 'TypeName' literal to every generated type. This is useful for Apollo Client's normalized cache but can cause unexpected type union narrowing issues. If you do not need inline fragment type narrowing, disable it with:

// codegen.ts
config: {
  skipTypename: true,  // omit __typename from generated types
}

Schema Changes Breaking Generated Types

When the server removes or renames a field, your operation files may still reference the old name. After running codegen, TypeScript will immediately flag the broken references. The fix is to update your operations to use the new field names. This is actually the system working correctly: schema drift is caught at compile time rather than at runtime in production.

Committing Generated Files

A common question is whether to commit the src/generated/ folder. The answer depends on your team workflow:

  • Commit generated files: Easier for onboarding, no codegen step needed to run TypeScript, CI can check for drift.
  • Gitignore generated files: Cleaner diffs, always regenerated fresh in CI. Requires codegen to run before tsc in every environment.
  • Most teams commit generated files and use the CI diff check pattern to catch stale types.

Frequently Asked Questions

What is @graphql-codegen?

@graphql-codegen (graphql-code-generator) is a CLI tool and plugin ecosystem that reads your GraphQL schema and operation files (queries, mutations, fragments) and generates TypeScript types, typed hooks, and client utilities automatically. It eliminates the need to manually write TypeScript interfaces that mirror your GraphQL schema. The generated code is always in sync with your schema, preventing runtime type mismatches between client and server.

How do I set up graphql-codegen?

Install the CLI and core plugins: npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations. Then create a codegen.ts (or codegen.yml) file at the project root. Point it to your schema (a URL, a .graphql file, or an introspection JSON) and your operation files (e.g., src/**/*.graphql). Run npx graphql-codegen to generate types. Add "codegen": "graphql-codegen" to your package.json scripts for convenience.

What is TypedDocumentNode?

TypedDocumentNode is a typed wrapper around the standard DocumentNode (from graphql-js) that carries TypeScript generic parameters for both the result type and the variables type. When you pass a TypedDocumentNode to Apollo Client, urql, or React Query's fetcher, TypeScript can automatically infer what data the query returns and what variables it requires, without any manual type annotation. It is generated by @graphql-codegen/typed-document-node and is the recommended approach for full end-to-end type safety.

How do I handle nullable fields from GraphQL?

GraphQL fields are nullable by default unless you add a ! non-null marker in the schema. @graphql-codegen faithfully represents this: a nullable field becomes T | null in TypeScript. You have two main strategies: (1) Use optional chaining (data?.user?.email) throughout your code, or (2) add avoidOptionals: true in codegen config to prefer T | null over T | undefined. In your schema, mark fields as non-null with ! whenever the server guarantees a value. Avoid the strictNullChecks: false escape hatch as it defeats the purpose.

What are custom scalars in GraphQL codegen?

GraphQL has built-in scalars (String, Int, Float, Boolean, ID) but schemas often define custom scalars like DateTime, UUID, or JSON. By default, @graphql-codegen maps unknown scalars to any. You should provide explicit TypeScript mappings in codegen.ts: config: { scalars: { DateTime: "string", UUID: "string", JSON: "Record<string, unknown>" } }. For Date objects, map DateTime to "Date" and handle the parsing in your network layer or with a custom scalar resolver.

Can I use graphql-codegen with React Query?

Yes. Install @graphql-codegen/typescript-react-query and add it to your codegen.ts generates block. It produces typed useQuery and useMutation hooks for each operation in your .graphql files. Configure the fetcher option to point to your fetch function or axios instance. The generated hooks are fully typed: calling useGetUserQuery() automatically provides TypeScript types for both the data and variables without any manual annotation. You can also use @tanstack/react-query v5 with the TypedDocumentNode approach for more flexibility.

How do I keep generated types up to date?

Use two complementary strategies: (1) During development, run graphql-codegen --watch to automatically regenerate types whenever a .graphql file or the schema changes. (2) In CI, add a codegen step before your TypeScript compiler check: run graphql-codegen, then tsc --noEmit. If the schema or operations change without regenerating types, the CI build will fail, preventing type drift from being merged. You can also set up a git pre-commit hook with husky + lint-staged to run codegen before commits.

What is the difference between TypedDocumentNode and DocumentNode?

DocumentNode is the base AST representation of a parsed GraphQL document. It carries no TypeScript type information about what the operation returns or what variables it takes. TypedDocumentNode<TResult, TVariables> extends DocumentNode with two generic parameters that encode the result and variables types. GraphQL client libraries (Apollo, urql, React Query) have overloads that detect TypedDocumentNode and use those generics to type the return value of useQuery, execute, etc. TypedDocumentNode is the recommended approach over inline type assertions or separate TypeScript interfaces.

Try GraphQL to TypeScript Online

Convert GraphQL SDL to TypeScript interfaces instantly in your browser — no install required.

Open GraphQL to TypeScript Tool
𝕏 Twitterin LinkedIn
Var dette nyttig?

Hold deg oppdatert

Få ukentlige dev-tips og nye verktøy.

Ingen spam. Avslutt når som helst.

Try These Related Tools

GTGraphQL to TypeScript{ }JSON FormatterTSJSON to TypeScriptJSON Validator

Related Articles

JSON til TypeScript Interface: Komplett Guide med Zod og Type Guards

Lær å generere TypeScript-interfaces fra JSON med zod og type guards.

OpenAPI til TypeScript: Komplett Guide til Kodegenerering med openapi-typescript

Lær å generere TypeScript-typer fra OpenAPI-specs med openapi-typescript.