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
| 比較項目 | REST + TypeScript | GraphQL + TypeScript |
|---|---|---|
| 型定義 | 手動でインターフェースを書く | スキーマから自動生成 |
| レスポンスの型安全 | 自前で維持・更新が必要 | codegen で常に最新を保証 |
| 必要なフィールドだけ取得 | 困難(over-fetching が発生) | クエリで指定したフィールドのみ型に反映 |
| API 仕様書 | 別途 OpenAPI などが必要 | スキーマ自体が仕様書になる |
| 型の変更検知 | ランタイムエラーになりやすい | コンパイル時に検出できる |
n
GraphQL スキーマの基本型と TypeScript の対応
n
# 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 型(イメージ)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 では
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.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
{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(自動生成・手動編集禁止)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
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.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.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
// 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
// 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(サブスクリプション対応版)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() はオペレーションの種別に応じてリンクを切り替えます。第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
// 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
// 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 のカスタムスカラー(DateTime・JSON・Upload など)は、codegen の設定でTypeScript の型にマッピングします。
n
よく使うカスタムスカラーの設定
n
// 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
// 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.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
// 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(サーバー設定)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.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.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 のモデル型(Prisma が生成した型)と GraphQL の型を分離できます。例えば DB の UserModel(password フィールドを含む)をリゾルバーの 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(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
// 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.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
// ページネーションクエリの型安全な使用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: 結果を楽観的に即座に表示する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
n
n
fetch を直接使うか、@apollo/experimental-nextjs-app-support パッケージを使います。Client Components('use client')であれば通常の Apollo Client が使えます。n
n
npm run codegen && git diff --exit-code src/generated/。これにより「スキーマを変更したのに codegen を再実行していない」というミスを防げます。または PR のチェックとして実行し、生成ファイルが最新でない場合は CI を失敗させる運用も効果的です。n
まとめ
n
TypeScript × GraphQL の型安全な開発フローをまとめます。
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