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

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

n

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

n

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

n

スポンサーリンク

GraphQL と TypeScript の親和性

n

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

n

n

n

n

n

n

n

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

n

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

n

schema.graphql
# GraphQL スキーマ定義ntype User {n  id: ID!            # string(non-null)n  name: String!      # string(non-null)n  email: String!     # string(non-null)n  age: Int           # number | null(nullable)n  role: UserRole!    # enumn  posts: [Post!]!    # Post[](non-null配列)n  createdAt: DateTime! # カスタムスカラー → Date にマッピングn}nnenum UserRole {n  ADMINn  USERn  GUESTn}nntype Query {n  user(id: ID!): User        # User | nulln  users: [User!]!            # User[]n}

n

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

n

GraphQL の ! は TypeScript の Non-Null に対応
GraphQL では String!! が「null を返さない」を意味します。codegen はこれを TypeScript の型に正確にマッピングし、! なし(nullable)は T | null! あり(non-null)は T として生成します。

n

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

n

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

n

インストール

n

ターミナル
# 基本パッケージnnpm install -D @graphql-codegen/cli @graphql-codegen/client-presetnn# TypeScript プラグイン(client-preset に含まれる)n# npm install -D @graphql-codegen/typescript @graphql-codegen/typescript-operationsnn# Apollo Client を使う場合nnpm install @apollo/client graphqlnn# urql を使う場合n# npm install urql @urql/core graphql

n

codegen.ts の設定

n

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

n

codegen.ts
// codegen.tsnimport type { CodegenConfig } from '@graphql-codegen/cli'nnconst config: CodegenConfig = {n  schema: 'http://localhost:4000/graphql',  // GraphQL サーバーのエンドポイントn  // または schema: './src/schema.graphql'  // ローカルのスキーマファイルnn  documents: ['src/**/*.graphql', 'src/**/*.tsx'],  // クエリを含むファイルnn  generates: {n    './src/generated/graphql.ts': {n      plugins: [],n      preset: 'client',n      config: {n        // カスタムスカラーの TypeScript 型マッピングn        scalars: {n          DateTime: 'Date',n          JSON: 'Record<string, unknown>',n          Upload: 'File',n        },n        // 生成する型の設定n        enumsAsTypes: true,  // enum を union type として生成n        avoidOptionals: false,n        nonOptionalTypename: true,n      }n    }n  }n}nnexport default config

n

package.json(scriptsに追加)
{n  "scripts": {n    "codegen": "graphql-codegen --config codegen.ts",n    "codegen:watch": "graphql-codegen --config codegen.ts --watch"n  }n}

n

ターミナル
# 型を生成(または更新)nnpm run codegennn# 開発中はウォッチモードで自動再生成nnpm run codegen:watch

n

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

n

src/generated/graphql.ts(生成例)
// src/generated/graphql.ts(自動生成・手動編集禁止)nexport type Maybe<T> = T | null;nexport type Scalars = {n  ID: { input: string; output: string; }n  String: { input: string; output: string; }n  Boolean: { input: boolean; output: boolean; }n  Int: { input: number; output: number; }n  Float: { input: number; output: number; }n  DateTime: { input: Date; output: Date; }  // カスタムスカラーn};nnexport type User = {n  __typename?: 'User';n  id: Scalars['ID']['output'];n  name: Scalars['String']['output'];n  email: Scalars['String']['output'];n  age?: Maybe<Scalars['Int']['output']>;n  role: UserRole;n  posts: Array<Post>;n  createdAt: Scalars['DateTime']['output'];n};nn// クエリの型(クエリごとに生成される)nexport type GetUserQueryVariables = Exact<{n  id: Scalars['ID']['input'];n}>;nnexport type GetUserQuery = {n  __typename?: 'Query';n  user?: {n    __typename?: 'User';n    id: string;n    name: string;n    email: string;n    role: UserRole;n  } | null;n};

n

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

n

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

n

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

n

Apollo Client のセットアップ

n

src/lib/apolloClient.ts
// src/lib/apolloClient.tsnimport { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client'nimport { onError } from '@apollo/client/link/error'nnconst httpLink = createHttpLink({n  uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ?? 'http://localhost:4000/graphql',n  credentials: 'include',  // Cookie 認証の場合n})nn// エラーハンドリングリンクnconst errorLink = onError(({ graphQLErrors, networkError }) => {n  if (graphQLErrors) {n    graphQLErrors.forEach(({ message, locations, path }) => {n      console.error(`[GraphQL error]: ${message}, Location: ${locations}, Path: ${path}`)n    })n  }n  if (networkError) {n    console.error(`[Network error]: ${networkError}`)n  }n})nnexport const apolloClient = new ApolloClient({n  link: from([errorLink, httpLink]),n  cache: new InMemoryCache({n    typePolicies: {n      User: {n        keyFields: ['id'],  // キャッシュのキー設定n      }n    }n  }),n})

n

型安全なクエリ(useQuery)

n

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

n

src/queries/user.graphql
# src/queries/user.graphqlnquery GetUser($id: ID!) {n  user(id: $id) {n    idn    namen    emailn    rolen    createdAtn  }n}nnquery GetUsers {n  users {n    idn    namen    emailn    rolen  }n}

n

UserProfile.tsx(useQuery の型安全使用)
// src/components/UserProfile.tsx(React × Apollo Client)nimport { useQuery } from '@apollo/client'nimport { graphql } from '@/generated'  // client-preset の場合nn// client-preset では gql タグの代わりに graphql() 関数を使うnconst GET_USER = graphql(`n  query GetUser($id: ID!) {n    user(id: $id) {n      idn      namen      emailn      rolen      createdAtn    }n  }n`)nninterface Props {n  userId: stringn}nnexport function UserProfile({ userId }: Props) {n  // useQuery に型引数は不要(GET_USER から自動推論)n  const { data, loading, error } = useQuery(GET_USER, {n    variables: { id: userId },  // { id: string } として型チェックされるn  })nn  if (loading) return <div>読み込み中...</div>n  if (error) return <div>エラー: {error.message}</div>nn  // data.user は GetUserQuery['user'] の型を持つn  // → { id: string; name: string; email: string; role: UserRole; createdAt: Date } | nulln  const user = data?.usern  if (!user) return <div>ユーザーが見つかりません</div>nn  return (n    <div>n      <h2>{user.name}</h2>  {/* user.name → string(型チェックあり)*/}n      <p>{user.email}</p>n      <span>{user.role}</span>  {/* UserRole 型として推論 */}n    </div>n  )n}

n

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

n

ミューテーション定義
# src/queries/user.graphql(追記)nmutation CreateUser($input: CreateUserInput!) {n  createUser(input: $input) {n    idn    namen    emailn    rolen  }n}nnmutation UpdateUser($id: ID!, $input: UpdateUserInput!) {n  updateUser(id: $id, input: $input) {n    idn    namen    emailn  }n}

n

CreateUserForm.tsx(useMutation の型安全使用)
// src/components/CreateUserForm.tsxnimport { useMutation } from '@apollo/client'nimport { graphql } from '@/generated'nimport { UserRole } from '@/generated/graphql'nnconst CREATE_USER = graphql(`n  mutation CreateUser($input: CreateUserInput!) {n    createUser(input: $input) {n      idn      namen      emailn      rolen    }n  }n`)nnexport function CreateUserForm() {n  // useMutation も CREATE_USER から型が自動推論されるn  const [createUser, { loading, error }] = useMutation(CREATE_USER, {n    // キャッシュの更新n    update(cache, { data }) {n      if (!data?.createUser) returnn      // キャッシュに新しいユーザーを追加n      cache.modify({n        fields: {n          users(existingUsers = []) {n            return [...existingUsers, data.createUser]n          }n        }n      })n    }n  })nn  async function handleSubmit(formData: FormData) {n    const name = formData.get('name') as stringn    const email = formData.get('email') as stringnn    try {n      const { data } = await createUser({n        variables: {n          input: {n            name,n            email,n            role: UserRole.User,  // 型安全な enum 値n          }n        }n      })n      // data.createUser の型は CreateUserMutation['createUser']n      console.log('作成されたユーザー:', data?.createUser.id)n    } catch (e) {n      console.error(e)n    }n  }nn  return (n    <form action={handleSubmit}>n      <input name="name" placeholder="名前" required />n      <input name="email" type="email" placeholder="メール" required />n      <button type="submit" disabled={loading}>n        {loading ? '作成中...' : 'ユーザー作成'}n      </button>n      {error && <p>エラー: {error.message}</p>}n    </form>n  )n}

n

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

n

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

n

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

n

ターミナル
npm install @apollo/client graphql-ws

n

src/lib/apolloClient.ts(WebSocket対応版)
// src/lib/apolloClient.ts(サブスクリプション対応版)nimport {n  ApolloClient, InMemoryCache,n  createHttpLink, split, fromn} from '@apollo/client'nimport { GraphQLWsLink } from '@apollo/client/link/subscriptions'nimport { createClient } from 'graphql-ws'nimport { getMainDefinition } from '@apollo/client/utilities'nnconst httpLink = createHttpLink({n  uri: process.env.NEXT_PUBLIC_GRAPHQL_HTTP_ENDPOINT ?? 'http://localhost:4000/graphql',n})nn// WebSocket リンク(サブスクリプション専用)nconst wsLink = new GraphQLWsLink(n  createClient({n    url: process.env.NEXT_PUBLIC_GRAPHQL_WS_ENDPOINT ?? 'ws://localhost:4000/graphql',n    connectionParams: () => ({n      // 認証トークンを WebSocket 接続時に送るn      Authorization: `Bearer ${localStorage.getItem('token') ?? ''}`,n    }),n  })n)nn// クエリ・ミューテーション → HTTP、サブスクリプション → WebSocket に振り分けnconst splitLink = split(n  ({ query }) => {n    const definition = getMainDefinition(query)n    return (n      definition.kind === 'OperationDefinition' &&n      definition.operation === 'subscription'n    )n  },n  wsLink,   // サブスクリプションは WebSocket へn  httpLink  // それ以外は HTTP へn)nnexport const apolloClient = new ApolloClient({n  link: from([splitLink]),n  cache: new InMemoryCache(),n})

n

split() によるリンク振り分け
split() はオペレーションの種別に応じてリンクを切り替えます。第1引数の判定関数が true を返す場合(サブスクリプション)は第2引数の wsLink を使い、false(クエリ・ミューテーション)の場合は第3引数の httpLink を使います。これにより1つの Apollo Client でHTTP通信とWebSocket通信を透過的に扱えます。

n

サブスクリプション定義
# リアルタイム通信(WebSocket 経由)nsubscription OnMessageAdded($roomId: ID!) {n  messageAdded(roomId: $roomId) {n    idn    contentn    sender {n      idn      namen    }n    createdAtn  }n}

n

ChatRoom.tsx(useSubscription)
// src/components/ChatRoom.tsxnimport { useSubscription } from '@apollo/client'nimport { graphql } from '@/generated'nnconst ON_MESSAGE_ADDED = graphql(`n  subscription OnMessageAdded($roomId: ID!) {n    messageAdded(roomId: $roomId) {n      idn      contentn      sender {n        idn        namen      }n      createdAtn    }n  }n`)nnexport function ChatRoom({ roomId }: { roomId: string }) {n  const { data, loading } = useSubscription(ON_MESSAGE_ADDED, {n    variables: { roomId },n  })nn  // data.messageAdded は OnMessageAddedSubscription['messageAdded'] 型n  const message = data?.messageAddednn  return (n    <div>n      {loading && <span>接続中...</span>}n      {message && (n        <div>n          <strong>{message.sender.name}</strong>: {message.content}n        </div>n      )}n    </div>n  )n}

n

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

n

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

n

フラグメント定義
# src/fragments/user.graphqlnfragment UserBasicFields on User {n  idn  namen  emailn}nnfragment UserDetailFields on User {n  ...UserBasicFieldsn  agen  rolen  createdAtn  posts {n    idn    titlen  }n}

n

UserCard.tsx(フラグメントマスキング)
// src/components/UserCard.tsxnimport { FragmentType, useFragment, graphql } from '@/generated'nn// フラグメントの型を定義(client-preset の useFragment パターン)nconst UserCardFragment = graphql(`n  fragment UserCardFields on User {n    idn    namen    emailn    rolen  }n`)nninterface Props {n  // FragmentType でフラグメントの型を参照n  user: FragmentType<typeof UserCardFragment>n}nnexport function UserCard({ user: userFragment }: Props) {n  // useFragment でデータを展開(フラグメントマスキング解除)n  const user = useFragment(UserCardFragment, userFragment)n  // user は { id: string; name: string; email: string; role: UserRole } として推論nn  return (n    <div className="user-card">n      <h3>{user.name}</h3>n      <p>{user.email}</p>n      <span>{user.role}</span>n    </div>n  )n}

n

フラグメントマスキングとは
client-preset の FragmentType + useFragment パターンは「フラグメントマスキング」と呼ばれます。親コンポーネントは子コンポーネントが必要とするフィールドを把握する必要がなく、コンポーネントが自分の必要なフィールドを宣言できます。これにより、「このコンポーネントには何のデータが必要か」が型レベルで明確になります。

n

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

n

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

n

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

n

codegen.ts(カスタムスカラー設定)
// codegen.ts の scalars 設定nconst config: CodegenConfig = {n  generates: {n    './src/generated/graphql.ts': {n      preset: 'client',n      config: {n        scalars: {n          // 日時n          DateTime: 'Date',n          Date: 'string',          // YYYY-MM-DD 形式の文字列の場合n          Time: 'string',nn          // JSON 型n          JSON: 'Record<string, unknown>',n          JSONObject: 'Record<string, unknown>',nn          // ファイルアップロードn          Upload: 'File',nn          // UUIDn          UUID: 'string',nn          // カスタムの厳密な型n          EmailAddress: 'string',n          URL: 'string',n          PositiveInt: 'number',n        }n      }n    }n  }n}

n

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

n

DateTime スカラーの変換リンク
// Apollo Link でカスタムスカラーを変換するnimport { ApolloLink } from '@apollo/client'nn// レスポンスの DateTime 文字列を Date オブジェクトに変換するリンクnfunction isDateString(value: unknown): value is string {n  return typeof value === 'string' && /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}/.test(value)n}nnfunction convertDates(obj: unknown): unknown {n  if (obj === null || obj === undefined) return objn  if (isDateString(obj)) return new Date(obj)n  if (Array.isArray(obj)) return obj.map(convertDates)n  if (typeof obj === 'object') {n    return Object.fromEntries(n      Object.entries(obj as Record<string, unknown>).map(n        ([key, value]) => [key, convertDates(value)]n      )n    )n  }n  return objn}nnconst dateConversionLink = new ApolloLink((operation, forward) => {n  return forward(operation).map((response) => {n    if (response.data) {n      response.data = convertDates(response.data) as typeof response.datan    }n    return responsen  })n})

n

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

n

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

n

ターミナル
npm install urql @urql/core graphqln# TypeScript の型定義は graphql パッケージに含まれる

n

src/lib/urqlClient.ts
// src/lib/urqlClient.tsnimport { createClient, cacheExchange, fetchExchange } from 'urql'nnexport const urqlClient = createClient({n  url: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT ?? 'http://localhost:4000/graphql',n  exchanges: [cacheExchange, fetchExchange],n  fetchOptions: () => ({n    headers: {n      Authorization: `Bearer ${getToken()}`,  // 認証トークンn    }n  })n})

n

UserList.tsx(urql の型安全使用)
// src/components/UserList.tsx(urql + client-preset)nimport { useQuery } from 'urql'nimport { graphql } from '@/generated'nnconst GET_USERS = graphql(`n  query GetUsers {n    users {n      idn      namen      emailn      rolen    }n  }n`)nnexport function UserList() {n  // urql の useQuery も codegen の型を自動推論n  const [result] = useQuery({ query: GET_USERS })n  const { data, fetching, error } = resultnn  if (fetching) return <div>読み込み中...</div>n  if (error) return <div>エラー: {error.message}</div>nn  return (n    <ul>n      {/* user は GetUsersQuery['users'][number] 型として推論 */}n      {data?.users.map(user => (n        <li key={user.id}>n          {user.name} ({user.email})n        </li>n      ))}n    </ul>n  )n}

n

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

n

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

n

サーバー用 codegen 設定

n

ターミナル
npm install -D @graphql-codegen/typescript-resolvers

n

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

n

src/types/context.ts
// src/types/context.tsnimport { PrismaClient } from '@prisma/client'nimport { User as UserModel } from '@prisma/client'nnexport interface GraphQLContext {n  prisma: PrismaClientn  currentUser: UserModel | nulln}

n

src/resolvers/userResolver.ts
// src/resolvers/userResolver.tsnimport { Resolvers } from '../generated/resolvers'nimport { GraphQLContext } from '../types/context'nn// Resolvers 型を使うとリゾルバーの引数・戻り値が型チェックされるnexport const userResolvers: Resolvers<GraphQLContext> = {n  Query: {n    // parent: unknown, args: { id: string }, context: GraphQLContextn    user: async (_parent, { id }, { prisma }) => {n      // prisma.user.findUnique の戻り値が User | nulln      return prisma.user.findUnique({ where: { id } })n    },n    users: async (_parent, _args, { prisma }) => {n      return prisma.user.findMany()n    },n  },nn  Mutation: {n    createUser: async (_parent, { input }, { prisma, currentUser }) => {n      // 認証チェックn      if (!currentUser) throw new Error('認証が必要です')nn      // input の型は CreateUserInput として推論されるn      return prisma.user.create({ data: input })n    },n  },nn  User: {n    // フィールドリゾルバー(関連データの解決)n    posts: async (parent, _args, { prisma }) => {n      // parent は UserModel 型として推論されるn      return prisma.post.findMany({ where: { authorId: parent.id } })n    },n  },n}

n

mappers でDB型とGraphQL型を分離する
mappers 設定により、DB のモデル型(Prisma が生成した型)と GraphQL の型を分離できます。例えば DB の UserModelpassword フィールドを含む)をリゾルバーの parent として使いつつ、GraphQL レスポンスの User 型(password を含まない)を別に定義できます。これにより機密情報の漏洩を型レベルで防止できます。

n

Next.js App Router との統合

n

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

n

Server Component でのデータフェッチ

n

src/app/users/page.tsx(RSC × GraphQL)
// src/app/users/page.tsx(Server Component)n// Apollo Client は RSC 非対応なので直接 fetch を使うnimport { GetUsersDocument, GetUsersQuery } from '@/generated/graphql'nimport { print } from 'graphql'  // DocumentNode を文字列に変換nnasync function fetchGraphQL<T, V extends Record<string, unknown>>(n  query: ReturnType<typeof print> | string,n  variables?: V,n  options?: RequestInitn): Promise<T> {n  const response = await fetch(n    process.env.GRAPHQL_ENDPOINT ?? 'http://localhost:4000/graphql',n    {n      method: 'POST',n      headers: { 'Content-Type': 'application/json' },n      body: JSON.stringify({ query, variables }),n      next: { revalidate: 60 },  // ISR: 60秒でキャッシュ更新n      ...options,n    }n  )nn  if (!response.ok) throw new Error(`GraphQL fetch failed: ${response.status}`)n  const { data, errors } = await response.json() as { data: T; errors?: unknown[] }n  if (errors) throw new Error(JSON.stringify(errors))n  return datan}nnexport default async function UsersPage() {n  // GetUsersQuery 型で型安全にデータを取得n  const data = await fetchGraphQL<GetUsersQuery, Record<string, never>>(n    print(GetUsersDocument)n  )nn  return (n    <ul>n      {data.users.map(user => (n        <li key={user.id}>{user.name}</li>n      ))}n    </ul>n  )n}

n

Client Component では Apollo Provider を使う

n

Apollo Provider の設定(Next.js App Router)
// src/components/providers/ApolloProvider.tsx('use client')n'use client'nnimport { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'nimport { ReactNode, useMemo } from 'react'nnexport function ApolloClientProvider({ children }: { children: ReactNode }) {n  const client = useMemo(n    () =>n      new ApolloClient({n        uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,n        cache: new InMemoryCache(),n      }),n    []n  )nn  return <ApolloProvider client={client}>{children}</ApolloProvider>n}nn// src/app/layout.tsx(Server Component)nimport { ApolloClientProvider } from '@/components/providers/ApolloProvider'nnexport default function RootLayout({ children }: { children: React.ReactNode }) {n  return (n    <html>n      <body>n        <ApolloClientProvider>{children}</ApolloClientProvider>n      </body>n    </html>n  )n}

n

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

n

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

n

src/types/graphqlError.ts
// src/types/graphqlError.tsnimport { GraphQLError } from 'graphql'nimport { ApolloError } from '@apollo/client'nn// GraphQL エラーコードの型定義ntype ErrorCode =n  | 'UNAUTHENTICATED'n  | 'FORBIDDEN'n  | 'NOT_FOUND'n  | 'BAD_USER_INPUT'n  | 'INTERNAL_SERVER_ERROR'nninterface GraphQLErrorExtensions {n  code: ErrorCoden  field?: string  // バリデーションエラーの場合n}nn// カスタムエラー型ninterface TypedGraphQLError extends GraphQLError {n  extensions: GraphQLErrorExtensionsn}nn// ApolloError からエラーコードを取得するユーティリティnexport function getErrorCode(error: ApolloError): ErrorCode | null {n  const gqlError = error.graphQLErrors[0] as TypedGraphQLError | undefinedn  return gqlError?.extensions.code ?? nulln}nn// 使用例nfunction handleMutationError(error: ApolloError) {n  const code = getErrorCode(error)n  switch (code) {n    case 'UNAUTHENTICATED':n      // ログインページにリダイレクトn      window.location.href = '/login'n      breakn    case 'FORBIDDEN':n      alert('この操作を行う権限がありません')n      breakn    case 'BAD_USER_INPUT':n      alert('入力内容を確認してください')n      breakn    default:n      alert('エラーが発生しました: ' + error.message)n  }n}

n

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

n

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

n

ページネーション スキーマ
# Cursor-based pagination(推奨)ntype PageInfo {n  hasNextPage: Boolean!n  hasPreviousPage: Boolean!n  startCursor: Stringn  endCursor: Stringn}nntype UserEdge {n  node: User!n  cursor: String!n}nntype UserConnection {n  edges: [UserEdge!]!n  pageInfo: PageInfo!n  totalCount: Int!n}nntype Query {n  users(first: Int, after: String, last: Int, before: String): UserConnection!n}

n

ページネーション実装(Apollo Client)
// ページネーションクエリの型安全な使用nconst GET_USERS_PAGINATED = graphql(`n  query GetUsersPaginated($first: Int!, $after: String) {n    users(first: $first, after: $after) {n      edges {n        node {n          idn          namen          emailn        }n        cursorn      }n      pageInfo {n        hasNextPagen        endCursorn      }n      totalCountn    }n  }n`)nnfunction UserListPaginated() {n  const { data, fetchMore } = useQuery(GET_USERS_PAGINATED, {n    variables: { first: 10 },n  })nn  function loadMore() {n    if (!data?.users.pageInfo.hasNextPage) returnnn    fetchMore({n      variables: {n        after: data.users.pageInfo.endCursor,n      },n      updateQuery(prev, { fetchMoreResult }) {n        return {n          users: {n            ...fetchMoreResult.users,n            edges: [n              ...prev.users.edges,n              ...fetchMoreResult.users.edges,n            ],n          },n        }n      },n    })n  }nn  return (n    <div>n      {data?.users.edges.map(({ node }) => (n        <div key={node.id}>{node.name}</div>n      ))}n      {data?.users.pageInfo.hasNextPage && (n        <button onClick={loadMore}>もっと見る</button>n      )}n    </div>n  )n}

n

型安全なキャッシュ操作

n

キャッシュ操作の型安全パターン
// Apollo Client のキャッシュを型安全に更新nimport { useApolloClient } from '@apollo/client'nimport { GetUsersDocument, GetUsersQuery, User } from '@/generated/graphql'nnfunction useAddUserToCache() {n  const client = useApolloClient()nn  return (newUser: User) => {n    // readQuery: キャッシュから型安全に読み取るn    const existing = client.readQuery<GetUsersQuery>({n      query: GetUsersDocument,n    })nn    if (!existing) returnnn    // writeQuery: キャッシュに型安全に書き込むn    client.writeQuery<GetUsersQuery>({n      query: GetUsersDocument,n      data: {n        users: [...existing.users, newUser],n      },n    })n  }n}

n

型安全な Optimistic UI

n

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

n

よくある質問(FAQ)

n

Qgraphql-codegen の代わりに手動で型を定義するのはどうですか?
A手動での型定義は非推奨です。スキーマが変更されたとき手動の型は更新されず、サイレントバグの原因になります。graphql-codegen は数分でセットアップでき、CI に組み込めばスキーマ変更を即座に型エラーとして検出できます。一度導入すると型の手動管理から解放され、開発速度が大きく向上します。

n

QApollo Client と urql はどちらを選べばよいですか?
A大規模アプリや高度なキャッシュ管理が必要なら Apollo Client、シンプルな実装や軽量さを重視するなら urql がおすすめです。Apollo Client は機能が豊富ですが学習コストが高く、バンドルサイズも大きめです(~30KB gzip)。urql はシンプルで拡張機能(Exchange)による柔軟なカスタマイズが可能です(~10KB gzip)。どちらも codegen との相性は良好です。

n

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 が使えます。

n

QGraphQL スキーマはどこで定義すべきですか(スキーマファースト vs コードファースト)?
A両方のアプローチがあります。スキーマファースト(SDL: Schema Definition Language で先にスキーマを書く)はフロント・バックエンドの並行開発がしやすく、コードファースト(TypeGraphQL などで TypeScript から生成)はバックエンドが TypeScript の場合に型の重複を避けられます。小〜中規模はスキーマファースト、バックエンドも TypeScript で統一している場合はコードファーストが多い印象です。

n

Qcodegen を CI/CD に組み込む方法は?
AGitHub Actions では以下のような設定が使えます:npm run codegen && git diff --exit-code src/generated/。これにより「スキーマを変更したのに codegen を再実行していない」というミスを防げます。または PR のチェックとして実行し、生成ファイルが最新でない場合は CI を失敗させる運用も効果的です。

n

まとめ

n

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

n

n

n

n

n

n

n

n

n

ステップ 内容 ツール・パターン
スキーマ定義 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 パターン

n

次のステップ
GraphQL × TypeScript をさらに深めるには、Zod 完全ガイドと組み合わせてミューテーション入力のバリデーションを追加したり、TypeScript × Next.jsのServer Actionsと組み合わせる方法を検討するのがおすすめです。また、ジェネリクス完全ガイドの知識があると codegen が生成するジェネリクス型(Exact<T> など)の理解が深まります。

n