Claude Media
Claude CodeでJavaScriptをTypeScriptに移行する段階手順

Claude CodeでJavaScriptをTypeScriptに移行する段階手順

allowJsで同居させ、型エラー数をPostToolUse hookで増やさない仕組みにして、strictまで段階的に進める手順。tsconfigとhookの設定例つき。

JavaScriptのプロジェクトをTypeScriptへ移すとき、Claude Codeに「全部変換して」と頼むと、大量の型エラーと巨大な差分が一度に返ってきます。止まりやすいのは変換そのものではなく、エラー数が減っているのか増えているのか誰にも分からなくなる点です。

この記事の進め方は3つの部品でできています。allowJsで.jsと.tsを同居させる。tsc --noEmitのエラー数を数字で記録する。PostToolUse hookでその数字を「増やさない」ゲートにする。この3つがあれば、Claude Codeが何ファイル触っても退行しません。最後にstrictを1項目ずつ有効にして仕上げます。

段階移行の全体像 — 4つの段階

各段階の終わりに、エラー数と残りの.jsファイル数を記録します。数字が戻らないことが、次へ進む条件です。

手順

JavaScriptからstrictまでの4段階

  1. 1

    段階0 同居させて計測する

    allowJsを有効にし、noEmitでtscを型チェック専用にします。既存のビルドはそのまま動きます。

  2. 2

    段階1 JavaScriptにも型検査をかける

    checkJsを有効にします。ここで出たエラー数が、最初の基準値になります。

  3. 3

    段階2 葉のモジュールから.tsにする

    依存される側のファイルから順に拡張子を変え、型注釈を足します。エラー数が増えたら差し戻します。

  4. 4

    段階3 strictを項目ごとに有効にする

    strictが束ねるオプションを個別に試し、エラー数が0になったものからtsconfig.jsonに書きます。

段階0: allowJsで同居させ、tscを型チェック専用にする

allowJsは、.tsと.tsxだけでなくJavaScriptファイルもプロジェクトに取り込めるようにするオプションです。TypeScriptのリファレンスは、JavaScriptのプロジェクトに.tsと.tsxを少しずつ足していく方法として、このフラグを挙げています。既定値はfalseですが、checkJsを設定すると有効になります。

noEmitは、JavaScriptやソースマップなどの出力を作らないオプションです。BabelやswcのようにTypeScriptを変換する別のツールがある場合に、tscは型チェッカーとして使えます。移行中はビルドを触らずに型の状態だけ見たいので、この組み合わせが合います。

tsconfig.jsonの最初の形は、例えば次のようになります。targetやmoduleは既存のビルド設定に合わせてください。

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "strict": false,
    "skipLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo"
  },
  "include": ["src"]
}

skipLibCheckは宣言ファイルの検査を省いて時間を縮める代わりに、型の正確さを一部手放します。移行の初期は自分のコードのエラーだけを数えたいので有効にしておき、strictまで終わった後で外せるか試す、という扱いが現実的です。incrementalは前回のコンパイル情報をディスクに保存するオプションで、毎回の編集後にtscを走らせるこの運用では再計算を減らす狙いで入れています。.tsbuildinfoは.gitignoreに追加します。

計測は次のコマンドです。エラー行はerror TSを含むので、行数を数えればエラー数になります。

npx tsc --noEmit --pretty false | grep -c 'error TS'
find src \( -name '*.js' -o -name '*.jsx' \) | wc -l

1行目が型エラー数、2行目が残りのJavaScriptファイル数です。この2つの数字が、以降のゲートの基準になります。

段階1: checkJsでJavaScriptの型エラーを可視化する

checkJsを有効にすると、JavaScriptファイルの型エラーも報告されます。リファレンスの説明では、対象のすべてのJavaScriptファイルの先頭に// @ts-checkを書いたのと同じ状態です。

一括で有効にすると数百件のエラーが出ることがあります。件数に驚いて止まらないよう、入口を2つ用意しておきます。

  • checkJs: trueにして、出たエラー数をそのまま最初の基準値として記録する。ゲートが「増やさない」だけを見るので、既存のエラーは残ったままで構いません
  • checkJs: falseのまま、直し終えたファイルにだけ// @ts-checkを足す。対象が広がるぶん数字が増えるため、基準値は足すたびに更新します

前者は数字が1本で済み、後者はファイル単位で小さく進められます。Claude Codeに任せるなら、数字が1本の前者のほうが指示が単純です。

「増やさない」ゲートをPostToolUse hookで置く

ここがこの進め方の中心です。PostToolUse hookは、ツール呼び出しが成功した後に動きます。Edit|Writeをmatcherにすれば、Claudeがファイルを書くたびに動かせます。

hookの終了コードが2のとき、PostToolUseではstderrがClaudeに届きます。ツールはすでに実行済みなので、止めるというより「直せ」というフィードバックを返す形です。巻き戻しはhookにはできません。したがってゲートの目的は、型エラーの増加を同じターンのうちにClaude自身へ返し、直させることにあります。

基準値は、リポジトリ直下の.ts-ratchetに1行で持ちます。1つ目がエラー数、2つ目が残りの.jsファイル数です。

212 87

次のスクリプトを.claude/hooks/ts-ratchet.shに置いて、実行権限を付けます。例として書いたもので、src以外にソースがあるならfindの対象を直してください。

#!/bin/bash
# .claude/hooks/ts-ratchet.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
 
# PostToolUseのときだけ、編集対象を絞る(--stop指定時は絞らない)
if [ "$1" != "--stop" ]; then
  case "$FILE" in
    *.ts|*.tsx|*.js|*.jsx) ;;
    *) exit 0 ;;
  esac
fi
 
cd "$CLAUDE_PROJECT_DIR" || exit 0
read -r BASE_ERR BASE_JS < .ts-ratchet
 
OUT=$(npx tsc --noEmit --pretty false 2>&1)
ERR=$(echo "$OUT" | grep -c 'error TS')
JS=$(find src \( -name '*.js' -o -name '*.jsx' \) | wc -l | tr -d ' ')
 
if [ "$ERR" -gt "$BASE_ERR" ] || [ "$JS" -gt "$BASE_JS" ]; then
  {
    echo "型エラー: ${ERR}件(基準 ${BASE_ERR})/ 残りJS: ${JS}件(基準 ${BASE_JS})"
    echo "基準を超えました。新しい.jsは作らず、増えたエラーを直してください。"
    echo "$OUT" | grep 'error TS' | head -20
  } >&2
  exit 2
fi
 
# 減っていたら基準値を更新する
if [ "$ERR" -lt "$BASE_ERR" ] || [ "$JS" -lt "$BASE_JS" ]; then
  echo "$ERR $JS" > .ts-ratchet
fi
exit 0

設定は.claude/settings.jsonに書きます。PostToolUseはファイル編集の直後、Stopは「終わった」と言う直前の検査として、同じスクリプトを2か所に置きます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ts-ratchet.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ts-ratchet.sh --stop"
          }
        ]
      }
    ]
  }
}

Stopを足す理由は、PostToolUseのEdit|Writeが拾えない書き換えがあるからです。Bashコマンドや外部プロセスが同じファイルを書き換えても、Edit|Writeのhookは動きません。拡張子を変えるgit mvはBashなので、ここを通ります。Stop hookは終了コード2でClaudeを作業に戻せるため、ターン終わりに数字が戻っていれば取りこぼしを拾えます。PostToolUse hookの組み立て方そのものはPostToolUse hookのLint自動修正に言語別の例があります。

「拡張子を変えたら数字が増える」問題を避ける順序

.jsを.tsにした瞬間にエラーが増える、という事態は、段階1を先にやっておくと起きにくくなります。checkJsが有効なら、JavaScriptの段階でも同じ規則で型検査されていて、拡張子を変えても数えるファイルの集合が変わらないからです。段階0から2へ飛ばすと、変換したファイルだけが新しく検査対象になり、変換のたびにゲートが赤になります。

段階2: 葉のモジュールからClaude Codeに.tsへ変換させる

変換の順序は、他から呼ばれない側ではなく、他のファイルから参照される側(ユーティリティ、定数、型の元になるデータ構造)を先にします。参照される側に型が付くと、呼び出し側のエラーが減りやすくなるためです。

作業の約束事はCLAUDE.mdに書いておくと、毎回の指示が短く済みます。例えば次のような内容です。

## TypeScript移行のルール
- 型検査は `npx tsc --noEmit`。エラー数は `.ts-ratchet` の値以下に保つ
- 新しいファイルは必ず `.ts` / `.tsx` で作る。`.js` を新規に作らない
- 変換は1回につき5ファイルまで。依存される側のファイルから進める
- 型エラーを `any` や `as` で黙らせない。直せない箇所はコメントで理由を残す
- 変換のたびに `git diff --stat` を見せ、`.ts-ratchet` の変化を報告する

CLAUDE.mdに「型検査を通す」と書くだけでは、毎回守られる保証はありません。ベストプラクティスのページも、Always typecheck before you finishのような行は「ほとんどの場合は効くが毎回ではない」と述べ、確実に守らせたい検査はStop hookで強制する形を勧めています。このため、約束事は文章で、強制はhookで、と役割を分けています。

依頼の文面は、範囲と終了条件を数字で書きます。

src/utils 配下の .js を .ts に変換して。1回5ファイルまで。
変換後に npx tsc --noEmit を実行し、エラー数が .ts-ratchet 以下であることを確認して。

機械的な繰り返しになる大量変換は、並列に分けられる作業です。コマンド側の使い方は/batchコマンドの使い方にまとめています。ただし並列で変換するほど、数字の推移が見えにくくなるため、小さな範囲で数字を確認しながら広げるほうが安全です。

数字だけを見ると見逃す抜け道

エラー数は目安であって、品質そのものではありません。anyを付けて黙らせれば数字は減ります。ゲートに頼る場合でも、次の指標を併せて持つと抜け道が減ります。

grep -rEn ': any\b|as any\b' src --include='*.ts' --include='*.tsx' | wc -l

この値が移行の途中で増えていれば、エラーを型で解決せず、型を放棄して消した可能性があります。.ts-ratchetの3つ目の数字にしておくのも1つの方法です。

段階3: strictを項目ごとに有効にして仕上げる

strictは、厳密な型検査のオプション群をまとめて有効にします。個別に外すこともできます。リファレンスによれば、将来のTypeScriptではstrictにオプションが追加される可能性があり、アップグレードで新しい型エラーが出ることがあります。

まとめて有効にせず、項目ごとにエラー数を測ってから決めます。コマンドラインのフラグは、tsconfig.jsonの値を上書きして試せます。

for f in noImplicitAny strictNullChecks strictFunctionTypes \
         strictPropertyInitialization useUnknownInCatchVariables; do
  printf '%s: ' "$f"
  npx tsc --noEmit --pretty false --$f | grep -c 'error TS'
done

出た数字が0のものは、そのままtsconfig.jsonにtrueで書きます。数字が大きいものは、ファイル単位で潰していきます。項目の意味は次のとおりです。

オプション検査すること
noImplicitAny検査すること型を推論できず暗黙のanyになる箇所をエラーにする
strictNullChecks検査することnullとundefinedを別の型として扱い、具体的な値が要る場所での使用をエラーにする
strictFunctionTypes検査すること関数の引数をより正確に検査する
strictPropertyInitialization検査することクラスのプロパティが宣言されたのにコンストラクタで設定されない場合にエラーにする
useUnknownInCatchVariables検査することcatch節の変数の型をanyでなくunknownにする

どれから進めるかは、手元の数字で決めます。エラーが多い項目は一度に有効にせず、先にゲートをその項目向けに作り直す手もあります。.ts-ratchetの基準を、そのフラグを付けたときのエラー数に差し替えれば、同じスクリプトが項目ごとのゲートになります。スクリプトのnpx tsc --noEmitに--noImplicitAnyを足すだけです。

すべてのフラグが0になったら、個別の行を消して"strict": trueに一本化します。ここで基準値を0にしたゲートは、そのまま回帰防止として残せます。

hookが止まらない・遅いときの確認先

  • 動かないとき: /hooksでPostToolUseの下にあるか確認します。matcherは大文字小文字を区別するため、Edit|Writeの綴りを確かめます
  • スクリプトが見つからないとき: $CLAUDE_PROJECT_DIRを使った絶対パスで書きます。jqが無い環境では、jq: command not foundが出るので事前にインストールします
  • 遅いとき: 編集のたびにプロジェクト全体を検査するため、大きなリポジトリでは待ち時間が積み上がります。commandタイプのhookの既定タイムアウトは10分で、timeoutで秒数を指定できます。Stop側にだけ全体検査を置き、PostToolUse側は軽い検査にする分担も取れます
  • 無限に差し戻されるとき: Stop hookが続けて8回止めると、次のブロックはClaude Code側が打ち切ってターンを終えます。上限はCLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられます。基準が現実とずれているなら、先に.ts-ratchetを直します

失敗時に操作ごと止めたい別の設計はhooksのonFailureで失敗時に操作を止める設定で扱っています。テストの自動実行を組み合わせるならhooksでテストを自動実行するも参考になります。

まとめ

移行の成否を決めるのは変換の速さより、数字が戻らない仕組みを先に置けたかどうかです。型エラー数と残りの.js数を.ts-ratchetに記録し、PostToolUseとStopの2か所でゲートにすれば、Claude Codeに任せる範囲を広げても退行を見逃しにくくなります。anyの数も併せて持てば、数字の稼ぎ方による見かけ上の改善も防げます。

Vueなどフレームワーク固有の型検査コマンドを使う場合は、検査コマンドをvue-tscに差し替える構成も取れます。具体例はVue 3アプリでvue-tscとVitestを検証コマンドにする記事にあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks-guide
  2. code.claude.com/docs/en/hooks
  3. code.claude.com/docs/en/best-practices
  4. typescriptlang.org/tsconfig
この記事を共有:XはてブLinkedIn