メインコンテンツへスキップ

GraphQL Codegen:GraphQL スキーマから型安全なコードを生成する方法

GraphQL Code Generator の実践的ガイド。セットアップと設定、生成される TypeScript 型と型付きドキュメントノード、Apollo・urql・graphql-request との統合、ウォッチモードや CI による自動化を解説。
更新日 2026年10月2日  · 12 分 読む

AIで探求

ChatGPTClaudePerplexity

GraphQL API 向けに手書きした TypeScript の型は、誰かがスキーマを変更した瞬間に古くなります。

たとえば、バックエンド開発者が Course 型の title フィールド名を name に変更したとします。フロントエンドは手書きの Course インターフェースが変更に気づかないためコンパイルは通りますが、バグは実行時まで発覚しません。現実のアプリケーションには多数のクエリ、ミューテーション、フラグメントが存在するのが普通で、この問題はあっという間に拡大します。

GraphQL Code Generator(略して GraphQL Codegen)は、スキーマと GraphQL のオペレーションを読み取り、型やクエリのヘルパー、クライアント統合、そして本来手で書くようなボイラープレートを自動生成します。TypeScript と Apollo、urql のような人気クライアントの組み合わせで最もよく使われます。スキーマが変わるたびに 1 コマンドで型を同期できます。

この記事では、小さな TypeScript プロジェクトに GraphQL Codegen をセットアップし、設定して、生成された型を使い、ワークフローの一部として自動で実行するまでの流れを解説します。

GraphQL は REST とどう違うのでしょうか?違いと利点は Graph QL vs REST: A Complete Guide をご覧ください。

GraphQL Codegen とは?

GraphQL Codegen は、GraphQL の定義をアプリケーションコードへ変換するコマンドラインツールです。

The Guild によるオープンソースプロジェクトで、プロジェクトに開発用依存としてインストールします。次の 3 つをつなぎます:

  • GraphQL スキーマ: サーバーとクライアント間の契約で、API が公開するすべての型・フィールド・引数を定義
  • GraphQL オペレーション: アプリケーションが API に送るクエリ、ミューテーション、サブスクリプション、フラグメント
  • 生成コード: スキーマとオペレーションに基づいて Codegen が書き出す型やヘルパー

スキーマは「何が可能か」を、オペレーションは「アプリケーションが何を要求するか」を Codegen に伝えます。

Codegen は両者を組み合わせ、各生成型をオペレーション 1 件のレスポンス形状に正確に一致させます。また、すべてのオペレーションをスキーマに対して検証します。冒頭のリネーム例なら、Cannot query field "title" on type "Course" のようなエラーで生成が止まり、実行前に不整合を検出できます。

出力はプラグインと設定に依存します。Codegen 自体は TypeScript を書きません。プラグインが担います。たとえば typescript プラグインはスキーマ型を、typescript-operations プラグインはクエリやミューテーションの型を生成します。Java や C# など他言語向けのプラグインもありますが、この記事では TypeScript に絞ります。

ここで覚えておきたいのは、Codegen は GraphQL サーバーの代わりにはならないということです。

クエリを解決したりデータを取得したりはしません。開発中に GraphQL の定義を読み取り、ファイルを書き出すだけです。あとはサーバーと GraphQL クライアントの役目です。

GraphQL Codegen の仕組み

どの Codegen セットアップも同じ流れに従います: スキーマ + GraphQL オペレーション -> Codegen 設定 -> 生成コード。

GraphQL Codegen のワークフローダイアグラム

GraphQL Codegen のワークフローダイアグラム

概念的には次のとおりです。

  1. スキーマを提供: Codegen にローカルの .graphql ファイル、または稼働中の GraphQL エンドポイントを指定します(後者はイントロスペクションで取得)

  2. オペレーションの場所を指定: src/**/*.ts のようなグロブで、コード内のクエリ、ミューテーション、フラグメントを見つけられるようにします

  3. プラグインと出力ターゲットを設定: どのファイルをどのプラグインで生成するかを指定します

  4. Codegen を実行: npx graphql-codegen を実行してスキーマを読み込み、オペレーションを検証し、出力を書き出します

  5. 生成物を利用: 生成された型やドキュメントノードをアプリケーションコードにインポートします

手順 1〜3 は通常 codegen.ts にまとめます。以下は講座カタログの最小構成です。

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

const config: CodegenConfig = {
  // Step 1: Where the schema comes from
  schema: "schema.graphql",
  // Step 2: Where your operations are placed
  documents: ["src/**/*.ts"],
  // Step 3: What to generate and where to write it
  generates: {
    "./src/gql/": { preset: "client" },
  },
};

export default config;

client プリセットは、スキーマ型、オペレーション型、型付きドキュメントノードを一括生成するプラグインの束です。セットアップと設定の節でそれぞれ取り上げます。

GraphQL Codegen のセットアップ方法

Codegen を理解する最良の方法は、実プロジェクトで動かしてみることです。

このセクションでは、ゼロから小さな講座カタログを作ります。コースと講師のスキーマ、1 つのクエリ、そしてクエリを TypeScript の型に変換する Codegen 設定を用意します。必要なのは Node.js と npm だけです。

ステップ 1:GraphQL Code Generator をインストール

新しいフォルダを作成し、npm プロジェクトを初期化します:

mkdir course-catalog && cd course-catalog
npm init -y

Codegen にはいくつかのパッケージが必要です。2 つは通常の依存関係、残りは開発時のみ使用するため dev 依存関係です:

npm install graphql @graphql-typed-document-node/core
npm install -D typescript @graphql-codegen/cli @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node

各パッケージの役割は次のとおりです:

  • graphql: スキーマやオペレーションのパースに Codegen が利用する、JavaScript 向けの GraphQL 参照実装

  • @graphql-typed-document-node/core: 生成コードがインポートする TypedDocumentNode 型を提供

  • @graphql-codegen/cli: Codegen のコマンドラインツール

  • @graphql-codegen/typescript-operations: クエリ、ミューテーション、フラグメントの型を生成するプラグイン

  • @graphql-codegen/typed-document-node: 型が付与されたドキュメントオブジェクトを生成するプラグイン

ステップ 2:設定ファイルを作成

Codegen はプロジェクトルートの設定ファイルから読み込みます。まずは空の codegen.ts を作成します:

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

const config: CodegenConfig = {};

export default config;

npx graphql-codegen init を実行して質問に答え、生成された設定ファイルを使う方法もあります。ここでは各設定の意味が分かるように手書きします。

ステップ 3:スキーマとドキュメントの場所を指定

Codegen には 2 つの入力が必要です。両方作成しましょう。

まず、プロジェクトルートに schema.graphql としてスキーマを保存します:

enum Level {
  BEGINNER
  INTERMEDIATE
  ADVANCED
}

type Instructor {
  id: ID!
  name: String!
  bio: String
}

type Course {
  id: ID!
  title: String!
  level: Level!
  durationHours: Int!
  instructor: Instructor!
}

type Enrollment {
  id: ID!
  course: Course!
  studentEmail: String!
}

type Query {
  courses(level: Level, limit: Int): [Course!]!
  course(id: ID!): Course
}

type Mutation {
  enrollStudent(courseId: ID!, studentEmail: String!): Enrollment!
}

次に、src/operations/courses.graphql を作成し、コースと講師を取得するクエリを書きます:

query GetCourses($level: Level, $limit: Int) {
  courses(level: $level, limit: $limit) {
    id
    title
    level
    instructor {
      name
    }
  }
}

両ファイルの場所を Codegen に指定します。schema キーにスキーマのパス、documents にオペレーションをマッチするグロブを渡します:

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
};

ステップ 4:出力ファイルを選ぶ

generates キーは出力先を指示します。内側の各キーがファイルパスです:

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
  generates: {
    "src/generated/graphql.ts": {},
  },
};

Codegen は src/generated/ フォルダがなければ作成します。

ステップ 5:関連プラグインを追加

プラグインがなければ出力は空です。インストールした 2 つを追加します:

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

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

export default config;

古いチュートリアルでは typescript プラグインも併記されているかもしれませんが、現在は不要です。typescript-operations 単体で動作し、実際に使われる Input、Enum、Operation 型のみを生成します。現行バージョンで同一出力に両方を入れると、TypeScript の重複宣言エラーになります。

ステップ 6:Codegen を実行

コマンドを覚えなくて済むよう npm スクリプトを追加します:

npm pkg set scripts.codegen="graphql-codegen"

実行します:

npm run codegen

Codegen はスキーマを読み込み、クエリを検証し、出力を書き出します:

Codegen の生成結果

Codegen の生成結果

もしオペレーションがスキーマに存在しないフィールドを参照していれば、ここで実行が失敗し、ファイルは書き出されません。

ステップ 7:生成結果を確認

src/generated/graphql.ts を開きます。次の内容が生成されています:

生成された出力

Codegen は次の 4 つを生成しました:

  • Level: enum のための文字列ユニオン。クエリで使われているためにのみ生成

  • GetCoursesQueryVariables: クエリに渡す変数の型。スキーマで必須指定がないためどちらも任意

  • GetCoursesQuery: レスポンスの形状。クエリが要求したフィールドのみを含む

  • GetCoursesDocument: 型付きドキュメントノードとしてのパース済みクエリ(結果型と変数型が付与)

GetCoursesQuery に再注目すると、durationHours や講師の bio は含まれていません。クエリが要求していないからです。コードで course.durationHours を読み取ろうとすれば、実行時の undefined ではなくコンパイルエラーになります。

以上がセットアップの全体像です。GraphQL を書けば、Codegen が TypeScript を書いてくれます。

GraphQL Codegen の設定方法

多くの Codegen 設定はここで示したものより長くなりますが、同じ 4 つの設定から成り立っています。

設定ファイルは codegen.ts、codegen.yml、codegen.json のいずれでも構いません。ここでは TypeScript を使うことで、設定自体に補完と型チェックが効きます。どの形式でも設定内容は同じです。

Schema(スキーマ)

schema キーはスキーマの所在を Codegen に伝えます。いくつかのソース形式を受け付けます。

リポジトリ内にある場合はローカルファイルやグロブを使います:

schema: "schema.graphql",
// or, for a schema split across files
schema: "schema/**/*.graphql",

稼働中サーバー上のスキーマには URL を使います。Codegen はエンドポイントにイントロスペクションクエリを送信し、認証ヘッダーも渡せます:

schema: [
  {
    "https://api.example.com/graphql": {
      headers: { Authorization: Bearer ${process.env.API_TOKEN} },
    },
  },
],

URL オプションは、サーバーでイントロスペクションが有効な場合のみ動作します。本番 API では無効化されていることも多いため、CI ではリポジトリにコミットしたスキーマファイルのほうが安全です。

Documents(ドキュメント)

documents キーは、クエリ、ミューテーション、サブスクリプション、フラグメントの所在を指定します。

オペレーションは .graphql ファイル内、または TypeScript のコード内にインラインで記述できます。インラインの場合、gql タグで囲むか /* GraphQL */ コメントでマークすると Codegen が検出します:

const EnrollStudent = /* GraphQL */ 
  mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
    enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
      id
      course {
        title
      }
    }
  }
;

両方を走査するにはグロブを広げます。Codegen 自身の出力を除外する否定パターンも加えます:

documents: ["src/**/*.{graphql,ts}", "!src/generated/**"],

すべてのオペレーションに一意な名前を付けてください。Codegen はオペレーション名から型名を組み立てるため、EnrollStudent は EnrollStudentMutation と EnrollStudentMutationVariables になります。

Generates(生成先)

generates は出力パスと内容の対応を定義します。必要なだけ出力ターゲットを持て、それぞれに個別のプラグインを設定できます。

たとえば、オペレーション型は 1 ファイルに、フルスキーマ型は別ファイルに分ける場合:

generates: {
  "src/generated/graphql.ts": {
    plugins: ["typescript-operations", "typed-document-node"],
    config: {
      useTypeImports: true,
    },
  },
  "src/generated/schema-types.ts": {
    plugins: ["typescript"],
  },
},

config はその出力ファイルのみに適用されます。ここでは useTypeImports により生成ファイルが import type を用いるようになります。ルートに config を置けばすべての出力に適用されます。

出力パスは "src/gql/" のようにスラッシュで終わるフォルダも指定できます。プリセットは複数ファイルを書き出すためこの形式を使います。

Plugins(プラグイン)

何を生成するかはプラグインで決まります。Codegen はスキーマとドキュメントを読み込み、各プラグインがそれらを出力へ変換します。

TypeScript プロジェクトでよく使うプラグインは次のとおりです:

  • typescript-operations: クエリ、ミューテーション、サブスクリプション、フラグメントの型、そしてそれらが用いる enum・input を生成

  • typed-document-node: 結果型と変数型が付与されたドキュメントオブジェクトを生成。多くの GraphQL クライアントが理解可能

  • typescript: スキーマ内のすべてのオブジェクト、enum、input の型を生成(オペレーションで未使用でも生成)

  • typescript-resolvers: サーバー側のリゾルバー関数の型シグネチャを生成

なお、プリセット(How It Works で触れた)client は、クライアントアプリ向けに The Guild が推奨するデフォルト設定のバンドルです。改良版 typescript-operations を土台にしているため、追加設定なしでその恩恵を得られます。

各プラグインには多数の設定オプションがありますが、最初から覚える必要はありません。まずはデフォルトで始め、生成物が期待と違う場合に該当オプションを調べれば十分です。

GraphQL Codegen は何を生成できるか?

Codegen の出力は選ぶプラグインに依存します。

同じスキーマとオペレーションでも、素の TypeScript 型からクライアント用のコードまで生成可能です。よく使う出力を紹介します。

TypeScript 型

typescript プラグインはスキーマ内のすべてのオブジェクト、enum、input に型を生成します。講座カタログの Course 型は次のようになります:

export type Course = {
  __typename?: 'Course';
  durationHours: Scalars['Int']['output'];
  id: Scalars['ID']['output'];
  instructor: Instructor;
  level: Level;
  title: Scalars['String']['output'];
};

これらはスキーマ全体を記述するため、サーバー側やテストフィクスチャ、完全なオブジェクトを扱うユーティリティ関数で有用です。

ただしクライアントコードには不向きです。Course 型はクエリで要求しないフィールドまで「存在する」とみなします。

オペレーション型

typescript-operations プラグインはオペレーションごとに 2 つの型(結果と変数)を生成します。クエリでもサブスクリプションでも同様です。

たとえば EnrollStudent ミューテーションでは次が生成されます:

export type EnrollStudentMutationVariables = Exact<{
  courseId: string | number;
  studentEmail: string;
}>;

export type EnrollStudentMutation = { enrollStudent: { id: string, course: { title: string } } };

結果型は選択セットをフィールド単位で忠実に反映します。変数型は Exact のため、余計な変数やスペルミスを TypeScript が拒否します。

courseId は string と number の両方を受け付けます。GraphQL の ID スカラーが入力として両方を受け付けるためです。v6 以降のデフォルト型は string | number で、クライアント用途として正しい型です。

型付きドキュメントノード

通常の DocumentNode はパース済み GraphQL クエリで、クエリの構造は分かりますが、返却値や変数の型は分かりません。

型付きドキュメントノードは両方の型情報を併せ持ちます。typed-document-node プラグインはオペレーションごとに 1 つ生成します:

export const GetCoursesDocument = { /* ... */ } as unknown as DocumentNode<GetCoursesQuery, GetCoursesQueryVariables>;

これにより、TypedDocumentNode を受け取る任意の関数は、ドキュメントだけから結果型と変数型を推論できます。ジェネリクスを手渡しする必要がなく、型の合わないクエリと組み合わせることもできません。

Apollo Client、urql、graphql-request を含む多くのモダンな GraphQL クライアントが型付きドキュメントノードを受け付けます。この記事の後半で 3 つすべてを実演します。

クライアント特化の統合

いくつかのプラグインは、特定のクライアントやフレームワーク用のコードまで生成します。例:

  • typescript-react-apollo: Apollo Client 用の型付き React フックを生成

  • typescript-urql: urql 用の型付きフックやコンポーネントを生成

  • typescript-react-query: TanStack Query 用の型付きフックを生成

  • typescript-graphql-request: オペレーションごとの関数を持つ型付き SDK を生成

これらはすべてコミュニティプラグインです。The Guild はそれらを別のコミュニティリポジトリへ移したため、リリースペースはコアプラグインと異なります。

また、以前より必要性は下がっています。クライアントが型付きドキュメントノードを受け付ける場合、生成フックが型安全性を大幅に高めるわけではありません。例えば Apollo の公式ドキュメントでも、追加のランタイムコードを含まず型の生成に集中する typescript-operations プラグインが推奨されています。

一般論としては、まずオペレーション型と型付きドキュメントノードから始め、チームが生成フックや SDK 関数を望む場合のみクライアント特化プラグインを追加してください。

TypeScript と GraphQL Codegen

生成された型が価値を持つのはミスを検出できるときです。ではわざとミスを作ってみましょう。

この例では、2 つ目のクエリ、API に送る小さなヘルパー、その結果を使うコードを追加します。http://localhost:4000/graphql でスキーマを実装する GraphQL サーバーが動作している前提です。

この例で使うスキーマの一部は次のとおりです:

type Instructor {
  id: ID!
  name: String!
  bio: String
}

type Course {
  id: ID!
  title: String!
  level: Level!
  durationHours: Int!
  instructor: Instructor!
}

type Query {
  course(id: ID!): Course
}

ここで重要なのは 2 点です。course フィールドは ! がないため、ID に一致するコースがないと null を返します。講師の bio も null 許容です。

src/operations/course.graphql を作成し、1 件のコースを取得するクエリを書きます:

query GetCourse($id: ID!) {
  course(id: $id) {
    title
    durationHours
    instructor {
      name
      bio
    }
  }
}

npm run codegen を再実行します。src/generated/graphql.ts に新しい 2 つの型が追加されます:

graphql.ts ファイルの内容

スキーマの null 許容がそのまま反映されます。course は null になり得て、bio も null になり得ます。id はクエリで ID! として宣言しているため必須です。

次にアプリケーションコードです。任意の型付きドキュメントを API に送るヘルパーを src/execute.ts に作成します:

import { print } from "graphql";
import type { TypedDocumentNode } from "@graphql-typed-document-node/core";

const API_URL = "http://localhost:4000/graphql";

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

このヘルパーはコースの知識を持ちません。渡されたドキュメントから両方の型を取得し、print() がドキュメントをリクエスト用のクエリ文字列に戻します。

そして src/course-details.ts で利用します:

import { execute } from "./execute";
import { GetCourseDocument, type GetCourseQuery } from "./generated/graphql";

function formatCourse(course: NonNullable<GetCourseQuery["course"]>): string {
  const bio = course.instructor.bio ?? "No bio yet";
  return course.title({course.durationHours}h) by ${course.instructor.name} - ${bio};
}

const { course } = await execute(GetCourseDocument, { id: "c1" });

if (course) {
  console.log(formatCourse(course));
} else {
  console.log("Course not found");
}

formatCourse() のパラメータ型は生成済みのクエリ型から得ています。NonNullable は null ケースを取り除き、if チェック後のみ関数を実行します。

この例はトップレベルの await を使うため、package.json に "type": "module" を設定し、npx tsx src/course-details.ts で実行します:

GraphQL モックサーバーへの呼び出し成功

GraphQL モックサーバーへの呼び出し成功

なお、ここでは GraphQL サーバーが動いている前提です。私が使ったモックは次のとおりです:

import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { createSchema, createYoga } from "graphql-yoga";

const instructors = [{ id: "i1", name: "Dario Radecic", bio: "Developer" }];

const courses = [
  { id: "c1", title: "Intro to GraphQL", level: "BEGINNER", durationHours: 4, instructor: instructors[0] },
  { id: "c2", title: "GraphQL Schema Design", level: "ADVANCED", durationHours: 6, instructor: instructors[0] },
];

const schema = createSchema({
  typeDefs: readFileSync("schema.graphql", "utf8"),
  resolvers: {
    Query: {
      courses: (_: unknown, args: { level?: string; limit?: number }) =>
        courses
          .filter((course) => !args.level || course.level === args.level)
          .slice(0, args.limit ?? undefined),
      course: (_: unknown, args: { id: string }) =>
        courses.find((course) => course.id === args.id) ?? null,
    },
    Mutation: {
      enrollStudent: (_: unknown, args: { courseId: string; studentEmail: string }) => ({
        id: "e1",
        course: courses.find((course) => course.id === args.courseId),
        studentEmail: args.studentEmail,
      }),
    },
  },
});

createServer(createYoga({ schema })).listen(4000, () => {
  console.log("Mock API running at http://localhost:4000/graphql");
});

では壊してみましょう。src/course-details.ts の formatCourse() 以降を、手書きの型では見逃しがちな 3 つの誤りに置き換えます:

// Missing the required id variable
await execute(GetCourseDocument, {});

const { course } = await execute(GetCourseDocument, { id: "c1" });

// Reading a field the query doesn't request
console.log(course?.level);

// Forgetting that course can be null
console.log(course.title);

そして型チェックを実行します:

npx tsc -p .

TypeScript が 3 つすべてを実行前に検出します:

TypeScript によって検出されたエラー

TypeScript によって検出されたエラー

これが Codegen がフロントエンドと API の不一致を減らす仕組みです。型はサーバーと同じスキーマから来るため、存在しないフィールドを記述したり、サーバーが返す可能性のある null を見落としたりしません。

しかもこの防御は初回だけで終わりません。たとえばバックエンドが durationHours を null 許容にしたとします。再生成すると型は number | null に変わり、数値を前提にしていた箇所をコンパイラがすべて指摘します。

GraphQL クライアントと GraphQL Codegen

Codegen はリクエストを送りません。それは GraphQL クライアントの仕事です。

生成された型付きドキュメントノードが両者の受け渡し点になります。これをクライアントに渡すと、クライアントが型を推論します。以下の例は先ほどの src/generated/graphql.ts とモック API を共通で使います。別ターミナルで npx tsx mock/server.ts を起動しておいてください。

Apollo と GraphQL Codegen

Apollo Client とそのピア依存の rxjs をインストールします:

npm install @apollo/client rxjs

この例では EnrollStudent ミューテーションも実行します。src/operations/enroll.graphql として保存します:

mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
  enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
    id
    course {
      title
    }
  }
}

Apollo のドキュメントでは Codegen 向けに 2 つの設定オプションも推奨しています。Apollo Client は常に __typename を含めますが、ルート型には追加しないためです。codegen.ts の出力ターゲットに追加します:

"src/generated/graphql.ts": {
  plugins: ["typescript-operations", "typed-document-node"],
  config: {
    nonOptionalTypename: true,
    skipTypeNameForRoot: true,
  },
},

これにより、生成される結果型のすべてのオブジェクトに必須の __typename フィールド(例:__typename: 'Course')が付き、Apollo のキャッシュと一致します。

npm run codegen を再実行し、新しいミューテーションと設定を反映します。

Apollo Client は query() と mutate() で型付きドキュメントノードを受け付けます。src/apollo.ts として保存します:

import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { EnrollStudentDocument, GetCoursesDocument } from "./generated/graphql";

const client = new ApolloClient({
  link: new HttpLink({ uri: "http://localhost:4000/graphql" }),
  cache: new InMemoryCache(),
});

const { data } = await client.query({
  query: GetCoursesDocument,
  variables: { level: "BEGINNER", limit: 5 },
});

data?.courses.forEach((course) => console.log(course.title));

const result = await client.mutate({
  mutation: EnrollStudentDocument,
  variables: { courseId: "c1", studentEmail: "student@example.com" },
});

console.log(result.data?.enrollStudent.course.title);

data は GetCoursesQuery | undefined として型付けされ、variables は GetCoursesQueryVariables に対してチェックされます。ミューテーションも EnrollStudentMutation とその変数で同様に動作します。

npx tsx src/apollo.ts で実行します:

Apollo の出力

Apollo の出力

1 行目はクエリの結果(初級コースが 1 件のみ)、2 行目はミューテーション(受講登録したコースのタイトル)が出力されています。

React の useQuery() や useMutation() といったフックも、型付きドキュメントノードから同様に型を推論します。

urql と GraphQL Codegen

urql のコアパッケージをインストールします:

npm install @urql/core

urql は型付きドキュメントノードをそのまま扱えます。src/urql.ts で query() の第 1 引数に渡します:

import { Client, cacheExchange, fetchExchange } from "@urql/core";
import { GetCoursesDocument } from "./generated/graphql";

const client = new Client({
  url: "http://localhost:4000/graphql",
  exchanges: [cacheExchange, fetchExchange],
});

const result = await client
  .query(GetCoursesDocument, { level: "ADVANCED" })
  .toPromise();

if (result.error) {
  throw result.error;
}

result.data?.courses.forEach((course) => console.log(course.title));

第 2 引数はクエリ変数の型になり、result.data はクエリ結果の型になります。

npx tsx src/urql.ts で実行します:

Urql の出力

Urql の出力

フレームワーク向けのバインディング(React の urql パッケージにある useQuery() など)も型付きドキュメントノードを受け付けます。

その他のクライアントでの利用

型付きの結果を得るのにフル機能のクライアントは必須ではありません。

graphql-request は 1 リクエストを送ってデータを返す最小限のクライアントです。以下でインストールします:

npm install graphql-request

Apollo や urql と同様に、型付きドキュメントノードから型を推論します。src/graphql-request.ts に保存します:

import { request } from "graphql-request";
import { GetCourseDocument } from "./generated/graphql";

const { course } = await request(
  "http://localhost:4000/graphql",
  GetCourseDocument,
  { id: "c2" },
);

console.log(course?.title);

npx tsx src/graphql-request.ts で実行します:

GraphQL request の出力

GraphQL request の出力

同じ発想は TanStack Query でも機能します。TanStack Query 自体は GraphQL リクエストを送らないため、クエリ関数内で graphql-request や前節の execute() ヘルパーを呼び、結果型をコードに流し込みます。

クライアントが型付きドキュメントノードを全くサポートしない場合でも、素のオペレーション型は使えます。GetCourseQuery と GetCourseQueryVariables をインポートしてジェネリクスとして渡してください。手作業は増えますが、型はスキーマ由来です。

GraphQL Codegen を自動で実行する方法

生成された型は、現在のスキーマとオペレーションに合致しているときにのみ役立ちます。

変更後に Codegen の再実行を忘れると、型はもはや存在しないコードを記述することになります。これは手書きの型と同じドリフト問題で、手順が増えただけです。解決策は、この手作業をワークフローから取り除くことです。

npm スクリプト

まず package.json にいくつかのスクリプトを用意し、チーム全員が同じ方法で Codegen を実行できるようにします:

{
  "scripts": {
    "codegen": "graphql-codegen",
    "codegen:watch": "graphql-codegen --watch",
    "codegen:check": "graphql-codegen --check",
    "prebuild": "npm run codegen",
    "build": "tsc -p ."
  }
}

以降では各スクリプトの用途を説明します。

ウォッチモード

ウォッチモードは監視対象ファイルの変更ごとに Codegen を再実行します。Codegen は自動でインストールしないため、@parcel/watcher パッケージが必要です:

npm install -D @parcel/watcher
npm run codegen:watch

インストールを省くと、Codegen は 1 回だけ実行され、監視の代わりに Failed to import @parcel/watcher と表示します。

ウォッチモードはオペレーションとローカルスキーマファイルの両方を追跡します。course.graphql にフィールドを追加すれば、1 秒後には GetCourseQuery にそのフィールドが現れます。schema.graphql で durationHours を null 許容にすると、型は number | null に変わり、数値を前提にしていた箇所をエディタが示します。

開発中は開発サーバーの隣で常時起動しておきましょう。

ビルド前に生成

npm は build の前に prebuild を実行します。上述のスクリプトなら、npm run build はまず型を再生成し、その後に TypeScript コンパイラを走らせます。

これで、誰かが変更後の再生成を忘れていても、ビルドが古い型で走ることはありません。

CI/CD

--check フラグはドライランで Codegen を実行します。ファイルは書き出さず、生成予定の内容とディスク上の内容を比較し、差分があれば終了コード 1 で終了します。

以下はそれを用いた最小の GitHub Actions ワークフローです:

name: CI

on: [push, pull_request]

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npm run codegen:check
      - run: npm run build

codegen:check は生成物をリポジトリにコミットしている場合にのみ意味があります。コミットしないならこの手順は不要で、prebuild により npm run build 時に再生成されます。

スキーマ変更時の再生成

オペレーションはリポジトリ内で変更されるため、ウォッチモードや prebuild でカバーできます。スキーマ変更は別チームから来ることが多く、少し厄介です。

対処はスキーマの所在によって異なります:

  • 同一リポジトリ内のスキーマファイル: ウォッチモードと CI が変化を拾い、最も簡単に運用できます
  • URL からのスキーマ: Codegen は毎回最新を取得するため、CI の codegen:check はサーバーのスキーマとコミット済み出力がずれた時点で失敗します
  • 別リポジトリが公開するスキーマ: ビルドの一部としてコピーまたはダウンロードし、ローカルコピーに対して Codegen を実行します

URL オプションには注意点が 1 つあります。ウォッチモードはファイル変更に反応するため、リモートスキーマの変更には気づきません。変更は手動実行か CI の次回ジョブで初めて反映されます。

GraphQL Codegen と手書きの型の比較

どちらのアプローチでも GraphQL データに対する TypeScript 型は得られますが、いくつかの違いを理解しておくべきです。

手書きの型

手書きの型では、スキーマを読み、対応するインターフェースを自分で作成します。

これにより完全なコントロールが得られます。型に任意の名前を付けたり、複数のオペレーションを 1 つの共有型にまとめたり、スキーマにない補助的な型を追加したりできます。

しかし手作業です。新しいフィールドや引数名の変更のたびに、対応する型を探して手で更新する必要があり、見落としても警告はありません。

これがドリフトの原因です。冒頭の Course インターフェースは、バックエンドが title をリネームしてもコンパイルできました。TypeScript は API ではなく自分の型に対してのみコードを検証するからです。

生成された型

生成された型はサーバーと同じスキーマから来るため、設計上 GraphQL の定義と同期しています。スキーマ変更と Codegen 実行の組み合わせで、影響を受ける型が一括更新されます。

また、記述量が大幅に減ります。講座カタログプロジェクトには 3 つのオペレーションがありますが、それらに対する手書きの型は 0 です。

さらに、スキーマドリブンな開発がほぼ自動で手に入ります。スキーマやオペレーションを変更して再生成すれば、コンパイラが変更すべき箇所を一覧で示してくれます。

ただし、Codegen は与えられたものをそのまま反映するだけ、という点は忘れないでください。

スキーマがすべてのフィールドを null 許容にしていれば、生成型は | null だらけになります。ページに 5 フィールドしか不要でも、オペレーションが 40 フィールドを取得していれば、生成型も 40 フィールドを持ちます。

違いを手短にまとめると次のとおりです:

  手書きの型 生成された型
保守 スキーマ変更のたびに手作業 変更のたびに 1 コマンド
スキーマドリフト 起こり得て静か 生成時またはコンパイル時に検出
ボイラープレート オペレーションの数に応じて増加 手書きは不要
カスタム型の自由度 完全 プラグイン設定の範囲内
スキーマドリブン開発 手動の相互確認 コンパイラが影響箇所を指摘

手書きの型と Codegen の比較

GraphQL Codegen と GraphQL イントロスペクション

Codegen はしばしばイントロスペクションを利用するため混同されがちですが、解決する問題は異なります。

イントロスペクション は GraphQL 仕様の機能です。任意の GraphQL サーバーは、__schema や __type といった特別なフィールドを通じて、自身のスキーマに関するクエリに応答できます。結果は API が公開するあらゆる型・フィールド・引数の JSON 記述です。

平たく言えば、イントロスペクションは「この API はどういう見た目か?」という問いに答えます。

Codegen は「アプリケーションに必要なコードは何か?」という問いに答えます。スキーマ情報とオペレーションを受け取り、TypeScript 型や型付きドキュメントノードを書き出します。

codegen.ts の schema に URL を設定すると、Codegen はそのサーバーにイントロスペクションクエリを送ってスキーマを取得します。ローカルの .graphql ファイルを使う場合、イントロスペクションは実行されません。

Codegen は出力としてイントロスペクション結果を生成することもできます。@graphql-codegen/introspection プラグインはスキーマをイントロスペクション JSON として書き出し、urql の Graphcache のようなツールがスキーマ認識キャッシュに利用します。

  イントロスペクション GraphQL Codegen
概要 GraphQL 組み込みのクエリ機構 コマンドラインツールとプラグイン
入力 稼働中サーバーへのクエリ スキーマとオペレーション
出力 スキーマの JSON 記述 型、ドキュメントノード、クライアントコード
実行タイミング クライアントがサーバーに問い合わせたときいつでも 開発・ビルド・CI の最中

イントロスペクションと Codegen の比較

GraphQL Codegen のよくある問題

多くの Codegen エラーは次の 4 つのいずれかに集約されます。

生成された型が見つからない

Codegen はエラーなく完了したのに、期待した型が出力ファイルにありません。

次の 3 点を順に確認してください:

  • スキーマ: Codegen が読み込むスキーマに型が存在しなければ生成できません。スキーマファイルを開くか自分でイントロスペクションクエリを投げて確認してください

  • ドキュメント: オペレーション型は Codegen が検出したオペレーションに対してのみ生成されます。クエリのあるファイルが documents のグロブにマッチしていなければ型は出力されません

  • プラグイン: v6 以降、typescript-operations はオペレーションが使用する enum と input のみを生成します。Course のようなフルスキーマ型が必要なら、typescript プラグインを別の出力ファイルに追加してください

未インストールのプラグインを設定した場合、Codegen は分かりやすいメッセージで失敗します:

Unable to find template plugin matching 'typescript-react-apollo'
Install one of the following packages:
- @graphql-codegen/typescript-react-apollo

Codegen がオペレーションを見つけられない

documents のグロブがオペレーションを含むファイルに一致しないと、このエラーになります:

Unable to find any GraphQL type definitions for the following pointers:
  - src/**/*.gql

グロブの拡張子がファイルに一致しているか確認してください。.gql パターンは .graphql を見つけられませんし、src/**/*.graphql は .ts 内のインラインオペレーションを見つけられません。

インラインオペレーションの場合はタグも確認します。Codegen は gql で包まれたテンプレートリテラルか、/* GraphQL */ コメントでマークされたものだけを検出します。プレーンなテンプレート文字列内のクエリは検出されません。

ignoreNoDocuments: true を設定すると、このエラーは出ず、オペレーション型のない出力が生成されます。新規プロジェクトでは有用ですが、後に壊れたグロブを見逃す可能性があります。

生成された型が古い

型はコンパイルできるが、最新のオペレーションやスキーマの変更を反映していません。

Codegen は自動では動きません。.graphql ファイル、インラインオペレーション、スキーマ、codegen.ts のいずれかを変更したら再実行してください。開発中は npm run codegen:watch が自動で再生成します。

CI で型が古い場合は、生成物をコミットするか、prebuild フックでビルド前に再生成してください。

生成された型が API と一致しない

型はコンパイルできるが、実行時のサーバーのデータ形状が異なります。

ほぼ確実に、Codegen とアプリが別のスキーマを参照しています。たとえば Codegen は 1 週間前のローカル schema.graphql を読み込んでいる一方で、アプリはスキーマがその後変更された本番 API を呼んでいる、といった状況です。

schema キーの参照先を確認してください。ファイルなら API が使うスキーマと比較し、更新します。URL なら、アプリが使う環境(本番・ステージングなど)と一致しているか確認してください。

その後で再生成し、差分を確認します。オペレーションファイルを変更したのに生成物が変わらない場合、documents のグロブ外にある可能性が高いです。

GraphQL Codegen のベストプラクティス

Codegen は多くの設定を必要としません。プロジェクトが大きくなっても、次の実践で快適さを保てます。

  • スキーマと生成物の同期を保つ: 開発中はウォッチモード、ビルド前は prebuild を走らせ、古いスキーマ由来の型で作業しないようにします

  • オペレーション固有の型を生成: クライアントコードには typescript-operations を使い、各クエリが取得する内容に正確一致させます

  • 生成物のコミットはワークフロー次第: コミットすれば PR で型の差分をレビューでき、CI の --check で陳腐化を検出できます。不要なら出力フォルダを .gitignore に入れ、ビルドごとに生成します

  • 設定は短く保つ: 多くのクライアントプロジェクトは 1 出力 + 2 プラグインで十分です。オプションを増やすほど、次の開発者が理解すべきことが増えます

  • 全プッシュで CI 上の Codegen を実行: --check か prebuild により、ローカルで再生成し忘れた変更を検出します

  • 生成ファイルを手で編集しない: 次回の Codegen 実行で上書きされます。修正はスキーマ、オペレーション、または設定側で行ってください

まとめ

GraphQL Codegen は GraphQL 開発の最も繰り返しの多い作業—スキーマとオペレーションから TypeScript 型を手で起こす作業—を肩代わりします。

プロジェクト規模がどれほど大きくなってもワークフローは同じです。スキーマとオペレーションを定義し、Codegen を設定し、型と型付きドキュメントノードを生成して、アプリケーションコードにインポートします。

最大の価値は、型が常に実際に呼び出す API を記述する点にあります。スキーマが変われば、コンパイラが問題のある箇所を教えてくれるため、早期にエラーを捕捉できます。

モダンな NoSQL データベースで GraphQL がどのように使われるか知りたい方は、MongoDB と GraphQL のブログ記事をご覧ください。

GraphQL Codegen のよくある質問

GraphQL Codegen は何に使われますか?

GraphQL Codegen は、GraphQL スキーマとオペレーションからコードを生成します。多くのチームは、クエリやミューテーションのための TypeScript 型と型付きドキュメントノードを得る目的で使い、手書きと更新の手間を省いています。サーバー向けのリゾルバー型や、特定クライアント向けのフックを生成することもできます。

Codegen の使用に実行中の GraphQL サーバーは必要ですか?

不要です。Codegen はローカルの .graphql ファイルからスキーマを読み取れるため、サーバーなしでも動作します。schema キーに URL を指定する場合のみ、イントロスペクションでスキーマを取得するため稼働中のサーバーが必要です。

生成ファイルを Git にコミットすべきですか?

チームのワークフロー次第です。生成物をコミットすれば、PR で型の変更をレビューでき、CI で graphql-codegen --check を使って古い出力を検出できます。不要であれば、出力フォルダを .gitignore に追加し、ビルド前に毎回再生成してください。

client プリセットと個別プラグイン、どちらを使うべきですか?

チームの用途によります。クライアントアプリには、オペレーション型・型付きドキュメントノード・フラグメントマスキングを束ねた client プリセットを The Guild は推奨しています。一方で Apollo は、プリセットがランタイムコードや Apollo Client と衝突する機能を追加するため、代わりに typescript-operations プラグインを推奨します。Apollo を使うなら個別プラグイン、そうでなければプリセットが無難です。

Codegen を更新したら重複宣言エラーが出るのはなぜですか?

v6 以降、typescript-operations プラグインは自身で enum と input を生成します。設定で同一出力ファイルに typescript と typescript-operations を併記していると、両方のプラグインが Level などを宣言し、TypeScript が重複宣言エラーを出します。該当出力から typescript を外すか、フルスキーマ型が必要なら別ファイルに分けてください。

トピック

DataCamp で学ぶ

コース

Software Development with Claude Code

4時間
8.8K
Claude Code brings AI assistance to your terminal. Learn the workflows that turn it into a reliable tool for real software development.
詳しく見るRight Arrow
コースを始める
もっと見るRight Arrow