【TypeScript × GraphQL】完全ガイド|graphql-codegen・Apollo Client・型安全なクエリ・スキーマ設計を徹底解説

【TypeScript × GraphQL】完全ガイド|graphql-codegen・Apollo Client・型安全なクエリ・スキーマ設計を徹底解説 HTML/CSS

REST API では毎回レスポンス型を手で書く必要がありますが、GraphQL × TypeScript では スキーマから型を自動生成することでその手間をゼロにできます。graphql-codegen を使えば、クエリ・ミューテーション・サブスクリプションの引数・レスポンスがすべて TypeScript の型として自動生成され、型安全なフロントエンド開発が実現します。

この記事では、GraphQL スキーマの基礎から graphql-codegen の設定・型自動生成、Apollo Client / urql での型安全なデータフェッチ、カスタムスカラーの型定義、Next.js との統合まで、実務レベルで使えるパターンを体系的に解説します。非同期処理の型定義の Promise パターンやジェネリクス完全ガイドの知識があると理解がより深まります。

この記事で学べること
GraphQL スキーマと TypeScript の型の対応関係、graphql-codegen による型・フック自動生成の設定方法、Apollo Client を使った型安全なクエリ・ミューテーション・サブスクリプション、urql での型定義パターン、カスタムスカラー型(Date・JSONなど)の TypeScript マッピング、フラグメントと型の再利用パターン、Next.js App Router との統合、サーバーサイド(schema-first)での TypeScript 型定義まで網羅
スポンサーリンク

GraphQL と TypeScript の親和性

GraphQL は スキーマが型定義の役割を兼ねているという点で TypeScript と非常に相性が良いです。スキーマで定義した型を TypeScript の型に変換することで、APIの変更が即座にコンパイルエラーとして検出されます。

比較項目 REST + TypeScript GraphQL + TypeScript
型定義 手動でインターフェースを書く スキーマから自動生成
レスポンスの型安全 自前で維持・更新が必要 codegen で常に最新を保証
必要なフィールドだけ取得 困難(over-fetching が発生) クエリで指定したフィールドのみ型に反映
API 仕様書 別途 OpenAPI などが必要 スキーマ自体が仕様書になる
型の変更検知 ランタイムエラーになりやすい コンパイル時に検出できる

GraphQL スキーマの基本型と TypeScript の対応

schema.graphql
# GraphQL スキーマ定義
type User {
  id: ID!            # string(non-null)
  name: String!      # string(non-null)
  email: String!     # string(non-null)
  age: Int           # number | null(nullable)
  role: UserRole!    # enum
  posts: [Post!]!    # Post[](non-null配列)
  createdAt: DateTime! # カスタムスカラー → Date にマッピング
}

enum UserRole {
  ADMIN
  USER
  GUEST
}

type Query {
  user(id: ID!): User        # User | null
  users: [User!]!            # User[]
}
自動生成される TypeScript 型(イメージ)
// ↑ スキーマから自動生成される TypeScript 型(イメージ)
export type User = {
  __typename?: 'User';
  id: string;
  name: string;
  email: string;
  age?: number | null;
  role: UserRole;
  posts: Post[];
  createdAt: Date;  // カスタムスカラーマッピング後
};

export enum UserRole {
  Admin = 'ADMIN',
  User = 'USER',
  Guest = 'GUEST'
}

export type Query = {
  __typename?: 'Query';
  user?: User | null;
  users: User[];
};
GraphQL の ! は TypeScript の Non-Null に対応
GraphQL では String!! が「null を返さない」を意味します。codegen はこれを TypeScript の型に正確にマッピングし、! なし(nullable)は T | null! あり(non-null)は T として生成します。

graphql-codegen のセットアップと型生成

graphql-codegen(@graphql-codegen/cli)は、GraphQL スキーマとクエリファイルから TypeScript の型・フックを自動生成するツールです。フロントエンド開発の必須ツールとして広く使われています。

インストール

ターミナル
# 基本パッケージ
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

# TypeScript プラグイン(client-preset に含まれる)
# npm install -D @graphql-codegen/typescript @graphql-codegen/typescript-operations

# Apollo Client を使う場合
npm install @apollo/client graphql

# urql を使う場合
# npm install urql @urql/core graphql

codegen.ts の設定

プロジェクトルートに codegen.ts(または codegen.yml)を作成します。現在は client-preset を使うのが推奨です。

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

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',  // GraphQL サーバーのエンドポイント
  // または schema: './src/schema.graphql'  // ローカルのスキーマファイル

  documents: ['src/**/*.graphql', 'src/**/*.tsx'],  // クエリを含むファイル

  generates: {
    './src/generated/graphql.ts': {
      plugins: [],
      preset: 'client',
      config: {
        // カスタムスカラーの TypeScript 型マッピング
        scalars: {
          DateTime: 'Date',
          JSON: 'Record<string, unknown>',
          Upload: 'File',
        },
        // 生成する型の設定
        enumsAsTypes: true,  // enum を union type として生成
        avoidOptionals: false,
        nonOptionalTypename: true,
      }
    }
  }
}

export default config
package.json(scriptsに追加)
{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.ts",
    "codegen:watch": "graphql-codegen --config codegen.ts --watch"
  }
}
ターミナル
# 型を生成(または更新)
npm run codegen

# 開発中はウォッチモードで自動再生成
npm run codegen:watch

生成されるファイルの構造

src/generated/graphql.ts(生成例)
// src/generated/graphql.ts(自動生成・手動編集禁止)
export type Maybe<T> = T | null;
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
  DateTime: { input: Date; output: Date; }  // カスタムスカラー
};

export type User = {
  __typename?: 'User';
  id: Scalars['ID']['output'];
  name: Scalars['String']['output'];
  email: Scalars['String']['output'];
  age?: Maybe<Scalars['Int']['output']>;
  role: UserRole;
  posts: Array<Post>;
  createdAt: Scalars['DateTime']['output'];
};

// クエリの型(クエリごとに生成される)
export type GetUserQueryVariables = Exact<{
  id: Scalars['ID']['input'];
}>;

export type GetUserQuery = {
  __typename?: 'Query';
  user?: {
    __typename?: 'User';
    id: string;
    name: string;
    email: string;
    role: UserRole;
  } | null;
};
client-preset の Exact 型について
Exact<T> は余分なフィールドを禁止する型です。type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] } と定義されており、変数に余計なプロパティを渡すとコンパイルエラーになります。これにより入力の安全性が保証されます。

Apollo Client での型安全なデータフェッチ

Apollo Client は React × GraphQL の最も広く使われるクライアントライブラリです。codegen と組み合わせることで、クエリ・ミューテーション・サブスクリプションすべてに型安全性をもたらします。

Apollo Client のセットアップ

src/lib/apolloClient.ts
// src/lib/apolloClient.ts
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client'
import { onError } from '@apollo/client/link/error'

const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ?? 'http://localhost:4000/graphql',
  credentials: 'include',  // Cookie 認証の場合
})

// エラーハンドリングリンク
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: ${message}, Location: ${locations}, Path: ${path}`)
    })
  }
  if (networkError) {
    console.error(`[Network error]: ${networkError}`)
  }
})

export const apolloClient = new ApolloClient({
  link: from([errorLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id'],  // キャッシュのキー設定
      }
    }
  }),
})

型安全なクエリ(useQuery)

クエリファイルを .graphql に分離し、codegen で型を生成するのが推奨パターンです。

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

query GetUsers {
  users {
    id
    name
    email
    role
  }
}
UserProfile.tsx(useQuery の型安全使用)
// src/components/UserProfile.tsx(React × Apollo Client)
import { useQuery } from '@apollo/client'
import { graphql } from '@/generated'  // client-preset の場合

// client-preset では gql タグの代わりに graphql() 関数を使う
const GET_USER = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      role
      createdAt
    }
  }
`)

interface Props {
  userId: string
}

export function UserProfile({ userId }: Props) {
  // useQuery に型引数は不要(GET_USER から自動推論)
  const { data, loading, error } = useQuery(GET_USER, {
    variables: { id: userId },  // { id: string } として型チェックされる
  })

  if (loading) return <div>読み込み中...</div>
  if (error) return <div>エラー: {error.message}</div>

  // data.user は GetUserQuery['user'] の型を持つ
  // → { id: string; name: string; email: string; role: UserRole; createdAt: Date } | null
  const user = data?.user
  if (!user) return <div>ユーザーが見つかりません</div>

  return (
    <div>
      <h2>{user.name}</h2>  {/* user.name → string(型チェックあり)*/}
      <p>{user.email}</p>
      <span>{user.role}</span>  {/* UserRole 型として推論 */}
    </div>
  )
}

型安全なミューテーション(useMutation)

ミューテーション定義
# src/queries/user.graphql(追記)
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
    role
  }
}

mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
  }
}
CreateUserForm.tsx(useMutation の型安全使用)
// src/components/CreateUserForm.tsx
import { useMutation } from '@apollo/client'
import { graphql } from '@/generated'
import { UserRole } from '@/generated/graphql'

const CREATE_USER = graphql(`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
      role
    }
  }
`)

export function CreateUserForm() {
  // useMutation も CREATE_USER から型が自動推論される
  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    // キャッシュの更新
    update(cache, { data }) {
      if (!data?.createUser) return
      // キャッシュに新しいユーザーを追加
      cache.modify({
        fields: {
          users(existingUsers = []) {
            return [...existingUsers, data.createUser]
          }
        }
      })
    }
  })

  async function handleSubmit(formData: FormData) {
    const name = formData.get('name') as string
    const email = formData.get('email') as string

    try {
      const { data } = await createUser({
        variables: {
          input: {
            name,
            email,
            role: UserRole.User,  // 型安全な enum 値
          }
        }
      })
      // data.createUser の型は CreateUserMutation['createUser']
      console.log('作成されたユーザー:', data?.createUser.id)
    } catch (e) {
      console.error(e)
    }
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="名前" required />
      <input name="email" type="email" placeholder="メール" required />
      <button type="submit" disabled={loading}>
        {loading ? '作成中...' : 'ユーザー作成'}
      </button>
      {error && <p>エラー: {error.message}</p>}
    </form>
  )
}

型安全なサブスクリプション(useSubscription)

サブスクリプション用 WebSocket リンクの設定

Apollo Client でサブスクリプションを使うには、HTTP リンクに加えて WebSocket リンクの設定が必要です。

ターミナル
npm install @apollo/client graphql-ws
src/lib/apolloClient.ts(WebSocket対応版)
// src/lib/apolloClient.ts(サブスクリプション対応版)
import {
  ApolloClient, InMemoryCache,
  createHttpLink, split, from
} from '@apollo/client'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
import { getMainDefinition } from '@apollo/client/utilities'

const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_HTTP_ENDPOINT ?? 'http://localhost:4000/graphql',
})

// WebSocket リンク(サブスクリプション専用)
const wsLink = new GraphQLWsLink(
  createClient({
    url: process.env.NEXT_PUBLIC_GRAPHQL_WS_ENDPOINT ?? 'ws://localhost:4000/graphql',
    connectionParams: () => ({
      // 認証トークンを WebSocket 接続時に送る
      Authorization: `Bearer ${localStorage.getItem('token') ?? ''}`,
    }),
  })
)

// クエリ・ミューテーション → HTTP、サブスクリプション → WebSocket に振り分け
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query)
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    )
  },
  wsLink,   // サブスクリプションは WebSocket へ
  httpLink  // それ以外は HTTP へ
)

export const apolloClient = new ApolloClient({
  link: from([splitLink]),
  cache: new InMemoryCache(),
})
split() によるリンク振り分け
split() はオペレーションの種別に応じてリンクを切り替えます。第1引数の判定関数が true を返す場合(サブスクリプション)は第2引数の wsLink を使い、false(クエリ・ミューテーション)の場合は第3引数の httpLink を使います。これにより1つの Apollo Client でHTTP通信とWebSocket通信を透過的に扱えます。
サブスクリプション定義
# リアルタイム通信(WebSocket 経由)
subscription OnMessageAdded($roomId: ID!) {
  messageAdded(roomId: $roomId) {
    id
    content
    sender {
      id
      name
    }
    createdAt
  }
}
ChatRoom.tsx(useSubscription)
// src/components/ChatRoom.tsx
import { useSubscription } from '@apollo/client'
import { graphql } from '@/generated'

const ON_MESSAGE_ADDED = graphql(`
  subscription OnMessageAdded($roomId: ID!) {
    messageAdded(roomId: $roomId) {
      id
      content
      sender {
        id
        name
      }
      createdAt
    }
  }
`)

export function ChatRoom({ roomId }: { roomId: string }) {
  const { data, loading } = useSubscription(ON_MESSAGE_ADDED, {
    variables: { roomId },
  })

  // data.messageAdded は OnMessageAddedSubscription['messageAdded'] 型
  const message = data?.messageAdded

  return (
    <div>
      {loading && <span>接続中...</span>}
      {message && (
        <div>
          <strong>{message.sender.name}</strong>: {message.content}
        </div>
      )}
    </div>
  )
}

フラグメントによる型の再利用

GraphQL のフラグメントはフィールドの集合を名前付きで再利用する仕組みです。codegen と組み合わせると、フラグメントごとに TypeScript の型が生成され、コンポーネント間の型共有が安全に行えます。

フラグメント定義
# src/fragments/user.graphql
fragment UserBasicFields on User {
  id
  name
  email
}

fragment UserDetailFields on User {
  ...UserBasicFields
  age
  role
  createdAt
  posts {
    id
    title
  }
}
UserCard.tsx(フラグメントマスキング)
// src/components/UserCard.tsx
import { FragmentType, useFragment, graphql } from '@/generated'

// フラグメントの型を定義(client-preset の useFragment パターン)
const UserCardFragment = graphql(`
  fragment UserCardFields on User {
    id
    name
    email
    role
  }
`)

interface Props {
  // FragmentType でフラグメントの型を参照
  user: FragmentType<typeof UserCardFragment>
}

export function UserCard({ user: userFragment }: Props) {
  // useFragment でデータを展開(フラグメントマスキング解除)
  const user = useFragment(UserCardFragment, userFragment)
  // user は { id: string; name: string; email: string; role: UserRole } として推論

  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <span>{user.role}</span>
    </div>
  )
}
フラグメントマスキングとは
client-preset の FragmentType + useFragment パターンは「フラグメントマスキング」と呼ばれます。親コンポーネントは子コンポーネントが必要とするフィールドを把握する必要がなく、コンポーネントが自分の必要なフィールドを宣言できます。これにより、「このコンポーネントには何のデータが必要か」が型レベルで明確になります。

カスタムスカラー型の TypeScript マッピング

GraphQL のカスタムスカラー(DateTimeJSONUpload など)は、codegen の設定でTypeScript の型にマッピングします。

よく使うカスタムスカラーの設定

codegen.ts(カスタムスカラー設定)
// codegen.ts の scalars 設定
const config: CodegenConfig = {
  generates: {
    './src/generated/graphql.ts': {
      preset: 'client',
      config: {
        scalars: {
          // 日時
          DateTime: 'Date',
          Date: 'string',          // YYYY-MM-DD 形式の文字列の場合
          Time: 'string',

          // JSON 型
          JSON: 'Record<string, unknown>',
          JSONObject: 'Record<string, unknown>',

          // ファイルアップロード
          Upload: 'File',

          // UUID
          UUID: 'string',

          // カスタムの厳密な型
          EmailAddress: 'string',
          URL: 'string',
          PositiveInt: 'number',
        }
      }
    }
  }
}

DateTime スカラーのシリアライズ・デシリアライズ

DateTime スカラーの変換リンク
// Apollo Link でカスタムスカラーを変換する
import { ApolloLink } from '@apollo/client'

// レスポンスの DateTime 文字列を Date オブジェクトに変換するリンク
function isDateString(value: unknown): value is string {
  return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
}

function convertDates(obj: unknown): unknown {
  if (obj === null || obj === undefined) return obj
  if (isDateString(obj)) return new Date(obj)
  if (Array.isArray(obj)) return obj.map(convertDates)
  if (typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj as Record<string, unknown>).map(
        ([key, value]) => [key, convertDates(value)]
      )
    )
  }
  return obj
}

const dateConversionLink = new ApolloLink((operation, forward) => {
  return forward(operation).map((response) => {
    if (response.data) {
      response.data = convertDates(response.data) as typeof response.data
    }
    return response
  })
})

urql での型安全なデータフェッチ

Apollo Client の代替として urql(ユアール)があります。よりシンプルな API と軽量さが特徴です。codegen との組み合わせ方は Apollo Client と似ています。

ターミナル
npm install urql @urql/core graphql
# TypeScript の型定義は graphql パッケージに含まれる
src/lib/urqlClient.ts
// src/lib/urqlClient.ts
import { createClient, cacheExchange, fetchExchange } from 'urql'

export const urqlClient = createClient({
  url: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ?? 'http://localhost:4000/graphql',
  exchanges: [cacheExchange, fetchExchange],
  fetchOptions: () => ({
    headers: {
      Authorization: `Bearer ${getToken()}`,  // 認証トークン
    }
  })
})
UserList.tsx(urql の型安全使用)
// src/components/UserList.tsx(urql + client-preset)
import { useQuery } from 'urql'
import { graphql } from '@/generated'

const GET_USERS = graphql(`
  query GetUsers {
    users {
      id
      name
      email
      role
    }
  }
`)

export function UserList() {
  // urql の useQuery も codegen の型を自動推論
  const [result] = useQuery({ query: GET_USERS })
  const { data, fetching, error } = result

  if (fetching) return <div>読み込み中...</div>
  if (error) return <div>エラー: {error.message}</div>

  return (
    <ul>
      {/* user は GetUsersQuery['users'][number] 型として推論 */}
      {data?.users.map(user => (
        <li key={user.id}>
          {user.name} ({user.email})
        </li>
      ))}
    </ul>
  )
}

サーバーサイド:スキーマファースト開発の型定義

サーバーサイドでも graphql-codegen を使ってリゾルバーの型を自動生成できます。スキーマで定義した型をリゾルバーが正しく実装しているかコンパイル時に検証できます。

サーバー用 codegen 設定

ターミナル
npm install -D @graphql-codegen/typescript-resolvers
codegen.ts(サーバー設定)
// codegen.ts(サーバー設定)
import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: './schema.graphql',
  generates: {
    // サーバー用の型生成
    './src/generated/resolvers.ts': {
      plugins: ['typescript', 'typescript-resolvers'],
      config: {
        contextType: '../types/context#GraphQLContext',  // コンテキスト型
        mappers: {
          // DB モデルとGraphQL型のマッピング
          User: '../models/user#UserModel',
          Post: '../models/post#PostModel',
        },
        scalars: {
          DateTime: 'Date',
        }
      }
    }
  }
}

export default config
src/types/context.ts
// src/types/context.ts
import { PrismaClient } from '@prisma/client'
import { User as UserModel } from '@prisma/client'

export interface GraphQLContext {
  prisma: PrismaClient
  currentUser: UserModel | null
}
src/resolvers/userResolver.ts
// src/resolvers/userResolver.ts
import { Resolvers } from '../generated/resolvers'
import { GraphQLContext } from '../types/context'

// Resolvers 型を使うとリゾルバーの引数・戻り値が型チェックされる
export const userResolvers: Resolvers<GraphQLContext> = {
  Query: {
    // parent: unknown, args: { id: string }, context: GraphQLContext
    user: async (_parent, { id }, { prisma }) => {
      // prisma.user.findUnique の戻り値が User | null
      return prisma.user.findUnique({ where: { id } })
    },
    users: async (_parent, _args, { prisma }) => {
      return prisma.user.findMany()
    },
  },

  Mutation: {
    createUser: async (_parent, { input }, { prisma, currentUser }) => {
      // 認証チェック
      if (!currentUser) throw new Error('認証が必要です')

      // input の型は CreateUserInput として推論される
      return prisma.user.create({ data: input })
    },
  },

  User: {
    // フィールドリゾルバー(関連データの解決)
    posts: async (parent, _args, { prisma }) => {
      // parent は UserModel 型として推論される
      return prisma.post.findMany({ where: { authorId: parent.id } })
    },
  },
}
mappers でDB型とGraphQL型を分離する
mappers 設定により、DB のモデル型(Prisma が生成した型)と GraphQL の型を分離できます。例えば DB の UserModelpassword フィールドを含む)をリゾルバーの parent として使いつつ、GraphQL レスポンスの User 型(password を含まない)を別に定義できます。これにより機密情報の漏洩を型レベルで防止できます。

Next.js App Router との統合

Next.js は GraphQL と組み合わせるケースが多いです。TypeScript × Next.jsで解説した App Router と組み合わせる方法を説明します。

Server Component でのデータフェッチ

src/app/users/page.tsx(RSC × GraphQL)
// src/app/users/page.tsx(Server Component)
// Apollo Client は RSC 非対応なので直接 fetch を使う
import { GetUsersDocument, GetUsersQuery } from '@/generated/graphql'
import { print } from 'graphql'  // DocumentNode を文字列に変換

async function fetchGraphQL<T, V extends Record<string, unknown>>(
  query: ReturnType<typeof print> | string,
  variables?: V,
  options?: RequestInit
): Promise<T> {
  const response = await fetch(
    process.env.GRAPHQL_ENDPOINT ?? 'http://localhost:4000/graphql',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables }),
      next: { revalidate: 60 },  // ISR: 60秒でキャッシュ更新
      ...options,
    }
  )

  if (!response.ok) throw new Error(`GraphQL fetch failed: ${response.status}`)
  const { data, errors } = await response.json() as { data: T; errors?: unknown[] }
  if (errors) throw new Error(JSON.stringify(errors))
  return data
}

export default async function UsersPage() {
  // GetUsersQuery 型で型安全にデータを取得
  const data = await fetchGraphQL<GetUsersQuery, Record<string, never>>(
    print(GetUsersDocument)
  )

  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Client Component では Apollo Provider を使う

Apollo Provider の設定(Next.js App Router)
// src/components/providers/ApolloProvider.tsx('use client')
'use client'

import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
import { ReactNode, useMemo } from 'react'

export function ApolloClientProvider({ children }: { children: ReactNode }) {
  const client = useMemo(
    () =>
      new ApolloClient({
        uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
        cache: new InMemoryCache(),
      }),
    []
  )

  return <ApolloProvider client={client}>{children}</ApolloProvider>
}

// src/app/layout.tsx(Server Component)
import { ApolloClientProvider } from '@/components/providers/ApolloProvider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ApolloClientProvider>{children}</ApolloClientProvider>
      </body>
    </html>
  )
}

GraphQL エラーの型安全なハンドリング

GraphQL のエラーは REST と異なる形式を持ちます。TypeScript でエラーを型安全に扱うパターンを解説します。Zod 完全ガイドによるバリデーションと組み合わせると、より堅牢な実装が可能です。

src/types/graphqlError.ts
// src/types/graphqlError.ts
import { GraphQLError } from 'graphql'
import { ApolloError } from '@apollo/client'

// GraphQL エラーコードの型定義
type ErrorCode =
  | 'UNAUTHENTICATED'
  | 'FORBIDDEN'
  | 'NOT_FOUND'
  | 'BAD_USER_INPUT'
  | 'INTERNAL_SERVER_ERROR'

interface GraphQLErrorExtensions {
  code: ErrorCode
  field?: string  // バリデーションエラーの場合
}

// カスタムエラー型
interface TypedGraphQLError extends GraphQLError {
  extensions: GraphQLErrorExtensions
}

// ApolloError からエラーコードを取得するユーティリティ
export function getErrorCode(error: ApolloError): ErrorCode | null {
  const gqlError = error.graphQLErrors[0] as TypedGraphQLError | undefined
  return gqlError?.extensions.code ?? null
}

// 使用例
function handleMutationError(error: ApolloError) {
  const code = getErrorCode(error)
  switch (code) {
    case 'UNAUTHENTICATED':
      // ログインページにリダイレクト
      window.location.href = '/login'
      break
    case 'FORBIDDEN':
      alert('この操作を行う権限がありません')
      break
    case 'BAD_USER_INPUT':
      alert('入力内容を確認してください')
      break
    default:
      alert('エラーが発生しました: ' + error.message)
  }
}

実務で使える型定義パターン集

ページネーションの型定義

ページネーション スキーマ
# Cursor-based pagination(推奨)
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type UserEdge {
  node: User!
  cursor: String!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type Query {
  users(first: Int, after: String, last: Int, before: String): UserConnection!
}
ページネーション実装(Apollo Client)
// ページネーションクエリの型安全な使用
const GET_USERS_PAGINATED = graphql(`
  query GetUsersPaginated($first: Int!, $after: String) {
    users(first: $first, after: $after) {
      edges {
        node {
          id
          name
          email
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      totalCount
    }
  }
`)

function UserListPaginated() {
  const { data, fetchMore } = useQuery(GET_USERS_PAGINATED, {
    variables: { first: 10 },
  })

  function loadMore() {
    if (!data?.users.pageInfo.hasNextPage) return

    fetchMore({
      variables: {
        after: data.users.pageInfo.endCursor,
      },
      updateQuery(prev, { fetchMoreResult }) {
        return {
          users: {
            ...fetchMoreResult.users,
            edges: [
              ...prev.users.edges,
              ...fetchMoreResult.users.edges,
            ],
          },
        }
      },
    })
  }

  return (
    <div>
      {data?.users.edges.map(({ node }) => (
        <div key={node.id}>{node.name}</div>
      ))}
      {data?.users.pageInfo.hasNextPage && (
        <button onClick={loadMore}>もっと見る</button>
      )}
    </div>
  )
}

型安全なキャッシュ操作

キャッシュ操作の型安全パターン
// Apollo Client のキャッシュを型安全に更新
import { useApolloClient } from '@apollo/client'
import { GetUsersDocument, GetUsersQuery, User } from '@/generated/graphql'

function useAddUserToCache() {
  const client = useApolloClient()

  return (newUser: User) => {
    // readQuery: キャッシュから型安全に読み取る
    const existing = client.readQuery<GetUsersQuery>({
      query: GetUsersDocument,
    })

    if (!existing) return

    // writeQuery: キャッシュに型安全に書き込む
    client.writeQuery<GetUsersQuery>({
      query: GetUsersDocument,
      data: {
        users: [...existing.users, newUser],
      },
    })
  }
}

型安全な Optimistic UI

Optimistic UI の実装
// Optimistic UI: 結果を楽観的に即座に表示する
const [deleteUser] = useMutation(DELETE_USER, {
  optimisticResponse: ({ id }) => ({
    __typename: 'Mutation' as const,
    deleteUser: {
      __typename: 'User' as const,
      id,
    },
  }),
  update(cache, { data }) {
    if (!data?.deleteUser) return
    // キャッシュから削除
    cache.evict({ id: cache.identify(data.deleteUser) })
    cache.gc()  // ガベージコレクション
  }
})

よくある質問(FAQ)

Qgraphql-codegen の代わりに手動で型を定義するのはどうですか?
A手動での型定義は非推奨です。スキーマが変更されたとき手動の型は更新されず、サイレントバグの原因になります。graphql-codegen は数分でセットアップでき、CI に組み込めばスキーマ変更を即座に型エラーとして検出できます。一度導入すると型の手動管理から解放され、開発速度が大きく向上します。
QApollo Client と urql はどちらを選べばよいですか?
A大規模アプリや高度なキャッシュ管理が必要なら Apollo Client、シンプルな実装や軽量さを重視するなら urql がおすすめです。Apollo Client は機能が豊富ですが学習コストが高く、バンドルサイズも大きめです(~30KB gzip)。urql はシンプルで拡張機能(Exchange)による柔軟なカスタマイズが可能です(~10KB gzip)。どちらも codegen との相性は良好です。
QNext.js の Server Components でも Apollo Client を使えますか?
AApollo Client は内部で React Context を使うため、現状 Server Components では直接使えません。RSC では fetch を直接使うか、@apollo/experimental-nextjs-app-support パッケージを使います。Client Components('use client')であれば通常の Apollo Client が使えます。
QGraphQL スキーマはどこで定義すべきですか(スキーマファースト vs コードファースト)?
A両方のアプローチがあります。スキーマファースト(SDL: Schema Definition Language で先にスキーマを書く)はフロント・バックエンドの並行開発がしやすく、コードファースト(TypeGraphQL などで TypeScript から生成)はバックエンドが TypeScript の場合に型の重複を避けられます。小〜中規模はスキーマファースト、バックエンドも TypeScript で統一している場合はコードファーストが多い印象です。
Qcodegen を CI/CD に組み込む方法は?
AGitHub Actions では以下のような設定が使えます:npm run codegen && git diff --exit-code src/generated/。これにより「スキーマを変更したのに codegen を再実行していない」というミスを防げます。または PR のチェックとして実行し、生成ファイルが最新でない場合は CI を失敗させる運用も効果的です。

まとめ

TypeScript × GraphQL の型安全な開発フローをまとめます。

ステップ 内容 ツール・パターン
スキーマ定義 GraphQL SDL でスキーマを書く .graphql ファイル
型自動生成 スキーマとクエリから TypeScript 型を生成 graphql-codegen(client-preset)
クエリ記述 graphql() 関数でクエリを定義 TypeDocument・フラグメント活用
データフェッチ useQuery/useMutation で型安全に取得 Apollo Client / urql
カスタムスカラー scalars 設定でTypeScript型にマッピング DateTime → Date など
サーバー側 Resolvers 型でリゾルバーを型安全に実装 typescript-resolvers
エラー処理 ErrorCode の union 型で型安全にハンドリング TypedGraphQLError パターン
次のステップ
GraphQL × TypeScript をさらに深めるには、Zod 完全ガイドと組み合わせてミューテーション入力のバリデーションを追加したり、TypeScript × Next.jsのServer Actionsと組み合わせる方法を検討するのがおすすめです。また、ジェネリクス完全ガイドの知識があると codegen が生成するジェネリクス型(Exact<T> など)の理解が深まります。