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 の対応
# 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 型(イメージ)
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 では
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
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
{
"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(自動生成・手動編集禁止)
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;
};
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
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
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
role
createdAt
}
}
query GetUsers {
users {
id
name
email
role
}
}
// 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
}
}
// 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(サブスクリプション対応版)
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() はオペレーションの種別に応じてリンクを切り替えます。第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
}
}
// 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
}
}
// 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 のカスタムスカラー(DateTime・JSON・Upload など)は、codegen の設定でTypeScript の型にマッピングします。
よく使うカスタムスカラーの設定
// 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 スカラーのシリアライズ・デシリアライズ
// 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
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()}`, // 認証トークン
}
})
})
// 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(サーバー設定)
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
import { PrismaClient } from '@prisma/client'
import { User as UserModel } from '@prisma/client'
export interface GraphQLContext {
prisma: PrismaClient
currentUser: UserModel | null
}
// 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 のモデル型(Prisma が生成した型)と GraphQL の型を分離できます。例えば DB の UserModel(password フィールドを含む)をリゾルバーの parent として使いつつ、GraphQL レスポンスの User 型(password を含まない)を別に定義できます。これにより機密情報の漏洩を型レベルで防止できます。Next.js App Router との統合
Next.js は GraphQL と組み合わせるケースが多いです。TypeScript × Next.jsで解説した App Router と組み合わせる方法を説明します。
Server Component でのデータフェッチ
// 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 を使う
// 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
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!
}
// ページネーションクエリの型安全な使用
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: 結果を楽観的に即座に表示する
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)
fetch を直接使うか、@apollo/experimental-nextjs-app-support パッケージを使います。Client Components('use client')であれば通常の Apollo Client が使えます。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> など)の理解が深まります。
