Claude Media
Claude CodeでReact Router v7アプリを開発する — typegenと型検査で完了判定

Claude CodeでReact Router v7アプリを開発する — typegenと型検査で完了判定

Claude CodeでReact Routerのフレームワークモードを開発するとき、react-router typegenとtscを完了条件にするCLAUDE.mdの書き方と、Stop hookで型エラーを残さない構成を解説します。

Claude CodeでReact Routerのフレームワークモードを開発するなら、完了条件は react-router typegen && tsc が通ることです。ルートごとの型は、実行前に生成するファイルから供給されます。その生成を飛ばしたまま tsc を走らせても、結果は何も保証しません。この記事では、型生成を前提にしたCLAUDE.mdの書き方と、Stop hookで型エラーを残したまま終われなくする構成を説明します。

React Routerのルート型は生成物でできている

React Routerは、アプリのルートごとに型を生成します。URLパラメーター、loaderの戻り値、actionの戻り値などが、ルートモジュールのexportから推論されます。

app/routes.ts に products/:id を登録した例で考えます。

import {
  type RouteConfig,
  route,
} from "@react-router/dev/routes";
 
export default [
  route("products/:id", "./routes/product.tsx"),
] satisfies RouteConfig;

ルートモジュール側は、生成された +types から Route 型を読み込みます。

import type { Route } from "./+types/product";
 
export function loader({ params }: Route.LoaderArgs) {
  //                      👆 { id: string }
  return { planet: `world #${params.id}` };
}
 
export default function Component({
  loaderData, // 👈 { planet: string }
}: Route.ComponentProps) {
  return <h1>Hello, {loaderData.planet}!</h1>;
}

./+types/product というファイルは、リポジトリには存在しません。React Routerが app/routes.ts を実行してルート構成を読み取り、.react-router/types/ ディレクトリに +types/<ルートファイル名>.d.ts を書き出します。rootDirs を設定すると、TypeScriptはそれをルートモジュールの隣にあるファイルとして解決します。

これがClaudeにとっての罠になります。型を供給する .d.ts が古いか未生成だと、params や loaderData の型は実態とずれるからです。Claudeが loader の戻り値を変えても、コンポーネント側の型は再生成されるまで追従しません。型検査を完了条件にするなら、検査の直前に生成を置く必要があります。

最初に整える3点

テンプレートから始めたプロジェクトでは、すでに設定済みのことが多い部分です。テンプレートを使わず自力で組んだプロジェクトでは、次の3点を確認します。

  1. .react-router/ を .gitignore に入れる
  2. tsconfigに生成型を含める
  3. 型生成を前段に置く typecheck スクリプトを用意する

.react-router/ は、React Routerが全面的に管理するディレクトリです。コミットの対象にしません。

.react-router/

tsconfigには include と rootDirs を足します。

{
  "include": [".react-router/types/**/*"],
  "compilerOptions": {
    "rootDirs": [".", "./.react-router/types"]
  }
}

package.json には、生成と検査を && でつないだスクリプトを置きます。

{
  "scripts": {
    "typecheck": "react-router typegen && tsc"
  }
}

複数のtsconfigを使うプロジェクトでは、変更する場所に注意が必要です。設定先は、アプリのディレクトリを include しているtsconfigです。React Routerのドキュメントに挙がっているnode-custom-serverテンプレートには tsconfig.json、tsconfig.node.json、tsconfig.vite.json の3つがあり、設定先は tsconfig.vite.json です。Claudeに設定を任せるときは、どのファイルが app/ を含むかを先に確認させます。

型はいつ自動で更新されるのか

型の更新経路は、実は2つあります。両者の違いを知らないと、Claudeが「開発サーバーを起動していないから型が古い」と誤解します。

経路型が生成されるタイミング
react-router dev(またはカスタムサーバーの vite.createServer)型が生成されるタイミングルート構成(routes.ts)を編集するたびにVite pluginが生成する
react-router typegen型が生成されるタイミング手動で実行したときに生成する
react-router typegen --watch型が生成されるタイミングファイルの変更を監視して再生成する

開発サーバーが動いている間は、型は最新に保たれます。一方、CIやhookのように開発サーバーを立てない場面では、react-router typegen を明示的に走らせる必要があります。--watch は、型生成だけを単独で常駐させたいときの選択肢です。

生成される型は、ルートごとに次の7種類です。

  • LoaderArgs
  • ClientLoaderArgs
  • ActionArgs
  • ClientActionArgs
  • HydrateFallbackProps
  • ComponentProps(default exportに対応)
  • ErrorBoundaryProps

CLAUDE.mdには、この一覧まで書く必要はありません。「各ルートモジュールで Route.LoaderArgs や Route.ComponentProps を使う」の1行があれば、Claudeは型を自前で書き起こさずに済みます。

CLAUDE.mdに書くReact Routerの規約

Claude Codeは、セッション開始時にCLAUDE.mdを読み込みます。書くべきことは、プロジェクト固有で、Claudeが推測しにくいものです。React Routerのフレームワークモードなら、次の3点が該当します。

以下はドキュメントの記載例ではなく、上の仕様から組み立てた一例です。

# React Routerアプリの規約
 
## 構成
- React Routerのフレームワークモード。ルート定義は `app/routes.ts`
- ルートモジュールは `app/routes/` に置き、`routes.ts` に必ず登録する
- 型は `./+types/<ルートファイル名>` から
  `import type { Route }` で読み込む
- `useLoaderData` や `useParams` より、`Route.ComponentProps` の
  `loaderData` と `params` を使う
- `.react-router/` は生成物。編集もコミットもしない
 
## 検証コマンド
- 型検査: `npm run typecheck`(内容は `react-router typegen && tsc`)
- 完了前に必ず実行し、エラー0にする
- ルート追加・変更後は `typegen` を挟まずに `tsc` だけを走らせない
- 起動確認: `npm run dev`(http://localhost:5173)

2行目の「Route.ComponentProps を使う」には根拠があります。ルートモジュールの解説によると、コンポーネントに loaderData、actionData、params、matches が渡される。useLoaderData や useParams の代わりに使え、ルートに合わせて型が付きます。フックで取るより、Propsで受けるほうが型の出どころが明確になります。

最後の起動確認で挙げた http://localhost:5173 は、インストール手順でテンプレートの起動後に案内されているアドレスです。ポートを変えているプロジェクトでは、実際の値に直します。

型検査では拾えない範囲

型検査が保証するのは、型の整合性までです。ルートの表示や、フォーム送信後のデータ再読み込みといった実行時の挙動は、起動して画面で確かめる必要があります。ドキュメントの例では、actionの完了後にloaderのデータが自動で再取得されます。型は通ってもこの動きが期待どおりかは、型検査では分かりません。CLAUDE.mdの検証コマンドの末尾に起動確認を残しておく理由はここにあります。

依頼文で完了条件を指定する

規約を書いたうえで、依頼文にも条件を入れると、手順の省略を減らせます。

/products/:id のルートを追加して。app/routes.ts への登録と
app/routes/product.tsx の作成の両方をやること。
完了前に npm run typecheck を実行し、エラーが残っていれば
修正して再実行して。エラー0になってから報告して。

ここで効くのは、routes.ts への登録を依頼文に明記している点です。React Routerの型生成は、routes.ts を実行して得たルート構成が起点です。ルートファイルだけを作って登録を忘れると、そのファイルには型が生成されません。登録漏れは、./+types/product が解決できないというエラーとして表に出るはずです。

Claudeが踏みやすい落とし穴

型生成を挟む構成には、いくつかの典型的なつまずきがあります。

症状原因の候補対処
./+types/... のimportが解決できない原因の候補typegen を実行する前に tsc を走らせた対処npm run typecheck を使う
新しいルートだけ型が付かない原因の候補routes.ts への登録漏れ対処route() で登録してから typegen を実行する
型は生成されるが解決されない原因の候補tsconfigの rootDirs と include が未設定対処前述のtsconfig設定を足す
設定したのに効かない原因の候補app/ を含まないtsconfigを編集した対処app/ を include しているファイルに設定する
生成物がコミットに混ざる原因の候補.react-router/ が .gitignore にない対処.gitignore に追加する

三つ目と四つ目は、見た目が似ていて取り違えやすい症状です。まず、include にも rootDirs にも問題がないか、編集したのが実際に app/ を含むtsconfigかを確認します。

verbatimModuleSyntax で型importを揃える

Claudeが型importを import type で書くかどうかは、プロジェクトによって揺れます。verbatimModuleSyntax を有効にすると、Route型のauto-importが import type になります。

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

有効にしない場合、auto-importは import { Route } from "./+types/my-route"; の形になります。有効にすると import type { Route } になり、バンドラーが型だけのモジュールを安全に除外できます。ドキュメント上も「任意」の設定です。Claudeの書くimportの形を揃えたいなら、有効にしておくと、CLAUDE.mdの規約に頼らなくても形が定まります。

Stop hookで型エラーを残したまま終わらせない

CLAUDE.mdと依頼文は、Claudeに守らせる「お願い」です。確実に強制するなら、Claude Codeのhookを使います。Stop hookは、Claudeが応答を終えるときに発火します。終了コード2を返すと、Claudeの停止が阻止されて会話が続きます。

.claude/settings.json に次を追加します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/typecheck.sh"
          }
        ]
      }
    ]
  }
}

.claude/hooks/typecheck.sh の中身は次のとおりです。

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
 
if ! OUT=$(npm run typecheck 2>&1); then
  echo "$OUT" | tail -n 40 >&2
  exit 2
fi
exit 0

stop_hook_active の確認は、hooksガイドが示す書き方です。Claude Codeは、Stop hookが8回連続でブロックし、その間にClaudeがツールを呼ばなかった場合に、hookを上書きして停止を許可します。確認を入れておくと、直せない型エラーで空回りすることを避けられます。

ただし、この確認には代償があります。hookで一度継続した後の2回目の停止要求は、検査せずに通ります。つまりこのhookが保証するのは「終わる前に型エラーを1度は突きつける」ことで、エラー0そのものではありません。粘らせたいときの調整方法は、CLAUDE_CODE_STOP_HOOK_BLOCK_CAPの解説にまとまっています。

hookをStopに置いたのは、typecheck がアプリ全体を対象にし、型生成まで含むからです。編集のたびに走らせると、ルート1つの変更で毎回全体を検査することになります。編集直後に個別ファイルへ走らせる検査は、PostToolUse hookのLint自動修正のように分けて考えます。

型が捕まえる変更の例

型検査が効く場面を、具体的に挙げます。以下は仕組みから導いた想定で、実際のエラーメッセージを再現したものではありません。

  • パラメーター名の変更: routes.ts で products/:id を products/:productId に直すと、params の型は { productId: string } に再生成される。ルートモジュール側が params.id を参照していれば、tsc が指摘する
  • loaderの戻り値の変更: loader が返すキーを変えると、Route.ComponentProps の loaderData の型も変わる。コンポーネントで古いキーを読んでいる箇所が型エラーになる
  • ルートの追加: routes.ts に新規ルートを登録すると、そのルート用の型が生成される

どれも、Claudeが片側だけを直したときに、もう一方の追従漏れとして表に出る種類のずれです。依頼文に「登録とルートファイルの両方」「loaderとコンポーネントの両方」と書くのは、型検査と組み合わせて初めて意味を持ちます。

CIにも同じコマンドを置く

typecheck スクリプトは、ドキュメントでもCIで型検査を単独実行する用途として紹介されています。ローカルのhookとCIで同じコマンドを共有すると、Claudeが直したエラーとCIで落ちるエラーが一致します。

GitHub Actionsの例です。これもドキュメントの記載例ではなく、構成の一例です。

name: typecheck
on: [pull_request]
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run typecheck

.react-router/ はgitignore対象なので、CIの作業ツリーには存在しません。そのため、npm run typecheck の中で react-router typegen を実行する構成が前提になります。tsc だけをCIに書くと、クローン直後は型が未生成のため、生成物を参照する箇所が壊れて見えます。

型を直すときにtsconfigを触らせない

型エラーが出続けると、Claudeはtsconfigの strict を緩めたり、include から対象を外したりして、エラーを消そうとすることがあります。React Routerの型検査は、include と rootDirs の2行に依存しています。ここが変わると、検査は通っても型は効かなくなります。

CLAUDE.mdに次の1行を足しておくと、回避策としての設定変更を抑えられます。

- 型エラーの回避のために tsconfig.json を変更しない。
  変更が必要なら、理由を説明して確認を取る

型の変更が正当な場面(verbatimModuleSyntax の有効化など)は、人が判断して依頼文で明示します。「Claudeに許可する変更」と「許可しない変更」を分けておくと、検証コマンドの結果を信頼できます。

起動して手動で型を再生成したいとき

hookやCIは npm run typecheck に任せますが、手元で原因を切り分けたいときは、生成と検査を別々に走らせると状況が分かりやすくなります。

npx react-router typegen
npx tsc

typegen の時点で失敗するなら、routes.ts の評価に問題があります。tsc だけが失敗するなら、型は生成できているので、コード側の不整合を疑います。この切り分けをClaudeに依頼文で伝えておくと、エラーの原因を絞る手間が減ります。

他のフレームワークとの違い

型生成が検査の前提になる構成は、他のフレームワークにもあります。Nuxtでは、自動インポートの型を作る nuxt prepare を検査の前に挟みます。詳しくはClaude CodeでNuxtアプリを開発するで扱っています。SvelteKitの sv check を使う手順はClaude CodeでSvelteKitを開発するにあります。

構造は共通していて、「型を生成するコマンド」と「型を検査するコマンド」の順序を、人でもClaudeでも間違えない場所に固定します。React Routerでは、その場所が typecheck スクリプトです。CLAUDE.mdには npm run typecheck の1行を書き、hookがそれを毎回走らせる。この分担にしておけば、依頼文が短くても、型が古いまま完了を宣言する事態は減らせます。

この記事を共有:XはてブLinkedIn