Claude Media
Claude CodeでnoImplicitAnyを有効にして暗黙のanyを段階的に潰す

Claude CodeでnoImplicitAnyを有効にして暗黙のanyを段階的に潰す

noImplicitAnyを一度に全体へ適用せず、ファイル単位で卒業させる運用を、tsconfigの分割とClaude Codeの検証ループ、anyの代わりに入れる型の決め方でまとめます。

noImplicitAnyは、型注釈がなく推論もできない箇所を、黙ってanyにせず型エラーにする設定です。既存コードに一度にかけると数百件のエラーが出ます。そこで、tsconfigをもう1枚作って「卒業したファイルだけ厳しく検査する」形にし、1ファイルずつClaude Codeに直させます。この記事はその運用と、anyの代わりに入れる型の決め方をまとめたものです。

noImplicitAnyは何を止める設定か

TypeScriptは、型注釈がなく推論もできない変数に対して、anyへフォールバックします。noImplicitAnyを有効にすると、anyと推論するはずだった箇所でエラーが出ます。公式ドキュメントの例は次のとおりです。

function fn(s) {
  console.log(s.subtr(3)); // typoだが、anyなので通ってしまう
}
fn(42);

オンにすると、引数 s に「Parameter 's' implicitly has an 'any' type」(エラー番号7006)が出ます。止まるのは暗黙のanyだけです。: any と明示した箇所は対象外で、あとから意図的な逃げ道として検索できます。

この設定は strict に含まれ、strict をtrueにすればまとめて有効になります。strict には strictNullChecks や useUnknownInCatchVariables なども入っています。一括で上げると原因の違うエラーが混ざるため、段階的に進める場合はフラグを個別に上げます。

全体に一度にかけない: ファイル単位で卒業させる

TypeScriptにはファイル単位でフラグを変える仕組みがありません。代わりに、設定ファイルを2枚に分けます。extends は継承元の設定を読み込んだうえで上書きでき、include を継承先で書けば継承元の include を置き換えられます。

// tsconfig.json(既存。noImplicitAnyは未設定のまま)
{
  "compilerOptions": { "strict": false },
  "include": ["src"]
}
// tsconfig.strict.json(卒業したファイルだけを載せる)
{
  "extends": "./tsconfig.json",
  "compilerOptions": { "noImplicitAny": true },
  "include": ["src/billing/**/*", "src/utils/date.ts"]
}

検査は tsc -p tsconfig.strict.json --noEmit で行い、卒業させたいファイルを include に足していきます。全ファイルが載ったら、フラグを本体の tsconfig.json に移して2枚目を消します。

卒業の進め方: 1ファイルずつ、検証付きで

卒業の手順を決めておくと、Claude Codeに同じ手順で繰り返し依頼できます。

手順

1ファイルを卒業させる手順

  1. 1

    件数を取る

    tsc -p tsconfig.strict.json --noEmit を、候補ファイルを足した状態で実行し、エラー件数を確認します。

  2. 2

    小さいものから選ぶ

    件数が少なく、他のファイルから呼ばれる側のファイルを先に選びます。

  3. 3

    型を入れて通す

    anyで埋めずに、実際の呼び出し元と使い方から型を決めて注釈します。

  4. 4

    strict側のincludeに足す

    エラーが0になったファイルを include に追加し、コミットします。

件数の取り方は次のとおりです。エラー番号7006は、前節の「引数が暗黙にanyになる」エラーです。

pnpm tsc -p tsconfig.strict.json --noEmit | grep -c "error TS"
pnpm tsc -p tsconfig.strict.json --noEmit | grep "TS7006" | head -20

PHPStanのlevelを1段ずつ上げ、古いエラーを隔離する運用と同じ考え方です。静的解析を段階導入する別の例はPHPStanの型エラーを減らす記事にあります。

Claude Codeに任せる範囲を決める

Claude Codeに「noImplicitAnyのエラーを全部直して」と渡すと、近道として any を明示して件数だけ減らす直し方が出がちです。これを避けるには、CLAUDE.mdに型の入れ方の規約と、検証コマンドを書いておきます。公式のベストプラクティスも、型検査のコマンドをClaudeが自分で確認できるようCLAUDE.mdに書くことを勧めています。例えば次のような形です。

# 型の移行ルール
- 検証: pnpm tsc -p tsconfig.strict.json --noEmit
- 1回の作業で触るファイルは1つ。対象はstrict側に追加するファイルのみ
- `any` を明示して通さない。外部入力は unknown にして絞り込む
- 直したら tsc を実行し、エラーが0になったことを確認してから終える
- 既存の // @ts-expect-error は、指示がない限り増やさない

規約を守らせたいなら、Stop フックで強制する方法があります。公式のベストプラクティスにある例は、tsc の出力をstderrに回して終了コード2で返す形です。この終了コードがClaudeの停止を止め、コンパイラの出力を理由として返します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command",
            "command": "pnpm tsc -p tsconfig.strict.json --noEmit 1>&2 || exit 2" }
        ]
      }
    ]
  }
}

連続8回ブロックしても進まない場合、Claude Codeは次のブロックを無効にしてターンを終えます。上限を上げる環境変数は CLAUDE_CODE_STOP_HOOK_BLOCK_CAP です。フックの全体像はClaude Codeのパワーユーザーtipsでも扱っています。

ファイルが多いときは、ヘッドレスモードで1ファイルずつ回します。--allowedTools に挙げたツールは確認なしで動きます。コマンドの範囲を絞った例は次のとおりです。

for f in $(cat next-batch.txt); do
  claude -p "tsconfig.strict.jsonのincludeに $f を足し、出るエラーを直して。CLAUDE.mdの型の移行ルールに従う" \
    --allowedTools "Read,Edit,Bash(pnpm tsc *)"
done

next-batch.txt には、その回に卒業させるファイルを1行ずつ書いておきます。1回の呼び出しで1ファイルに絞ると、差分のレビューが楽になります。

anyの代わりに入れる型の決め方

型を決める順番は、情報の出どころで分けると迷いません。

コードの状況入れる型見るべき場所
関数の引数で、呼び出し元が限られる入れる型呼び出し元で渡している値の型見るべき場所呼び出し箇所を検索
外部入力(JSON、環境変数、APIレスポンス)入れる型unknown にして絞り込む見るべき場所検証関数かスキーマ検証
catch の変数入れる型unknown(Error判定してから使う)見るべき場所useUnknownInCatchVariables
コールバックや型付きライブラリの引数入れる型ライブラリの型定義から取る見るべき場所関数の型を引く
本当に何でも入る入れる型unknown、または型引数見るべき場所呼び出し側の絞り込み

unknown は「何でも入るが、そのまま使えない型」です。catch の例では、err instanceof Error を確認してからでないと err.message を読めません。anyはこの確認を飛ばせるため、あとから型エラーが表に出ません。

catch の変数は専用のフラグがあり、useUnknownInCatchVariables を有効にすると、catch の変数がanyではなくunknownになります。これは noImplicitAny とは別のフラグなので、strict 側へ移るときに別件として上げます。

Claude Codeには「呼び出し元を検索して、実際に渡されている値から型を決める」ことを求めます。実装を読んで推測した型は、呼び出し側で使われ方と合わないことがあります。型を決めたら tsc で確かめる、までを1回の依頼にまとめると、確認漏れが減ります。

ts-migrateが残した@ts-expect-errorの後始末

JavaScriptからの移行にts-migrateを使った場合は、別の後始末があります。ts-migrateはプロジェクトを「ビルドが通る状態」にするツールです。READMEは、その結果に // @ts-expect-error と any が大量に残り、型安全性のためには後続の修正が必要だと説明しています。

@ts-expect-error は、直後の行にエラーがなければ「Unused '@ts-expect-error' directive」として報告されます。同じ位置に @ts-ignore を置くと、エラーがなくても何も言われません。TypeScript 3.9のリリースノートは、すぐ直す予定の回避策や、抑制コメントをできるだけ早く消したいチームには @ts-expect-error を選ぶよう案内しています。

この挙動は、後始末に使えます。noImplicitAnyを上げて型を直すと、その行のコメントが不要になり、未使用ディレクティブとして報告されます。次の流れです。

  1. 卒業させるファイルの @ts-expect-error の数を数える
  2. 型を直して tsc を実行する
  3. 「Unused '@ts-expect-error' directive」と報告された行のコメントを消す
  4. もう一度 tsc を実行し、エラーが0になるのを確認する
grep -rn "@ts-expect-error" src/billing | wc -l
pnpm tsc -p tsconfig.strict.json --noEmit | grep "Unused '@ts-expect-error'"

Claude Codeには、この3、4を依頼します。コメントを消す対象は、tsc が未使用と報告した行に限定します。報告されていない行のコメントは、まだ実際のエラーを隠しているので、消すとビルドが落ちます。

よくあるつまずき

  • 一度に strict: true にする: 件数が多すぎて、レビューも進捗も追えなくなります。フラグは個別に、ファイルは1つずつ上げます
  • 卒業済みファイルが未卒業ファイルをimportしている: 前述のとおり、import先もstrict側の検査対象に入ります。末端のファイルから進めるか、先に未卒業側を直します
  • any の明示で件数を減らす: noImplicitAnyは暗黙のanyだけを止めます。明示すると通るため、CLAUDE.mdの規約で禁止しておきます
  • フックのブロックが繰り返し無効になる: 連続8回ブロックしても進展がないと、Claude Codeは次のブロックを無効にして終了します。エラーの数が多すぎるファイルは分割します

React RouterやNuxtのように、フレームワークが型を自動生成する場合は、生成した型を先に整えると any が減ります。React Router v7のtypegenやNuxtのnuxt typecheckの記事が参考になります。

卒業後に上げるフラグと、進み具合の測り方

strict に含まれるフラグは、strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitAny、noImplicitThis、useUnknownInCatchVariables などです。それぞれ原因の違うエラーを出すので、noImplicitAnyが落ち着いたあとも同じ形で1つずつ上げます。たとえば tsconfig.strict.json の compilerOptions に noImplicitThis を足し、直ったファイルから本体の設定へ移す、という流れです。フラグを増やすたびに、全卒業ファイルが再検査されます。

進み具合は、卒業ファイル数を全体のファイル数で割るだけで足ります。ふだん使うシェルで十分です。

git ls-files 'src/**/*.ts' 'src/**/*.tsx' | wc -l
pnpm tsc -p tsconfig.strict.json --noEmit --listFiles | grep -c "/src/"

後者の数には、import経由で検査に入ったファイルも含まれます。include に書いたファイル数との差が大きいときは、卒業側が未卒業のファイルに依存しています。その差を、次に直す候補の一覧として使えます。

まとめ

noImplicitAnyの導入は、ファイルを1つずつ卒業させる運用にすると、レビューできる大きさで進みます。卒業の判定は tsconfig.strict.json の tsc に任せ、Claude Codeには「anyで通さない」規約と検証コマンドを渡します。ts-migrateが残した @ts-expect-error は、未使用の報告を手がかりに消していけます。最後に本体の tsconfig.json へフラグを移せば、2枚目の設定は不要になります。

参照した一次ソース

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

  1. code.claude.com/docs/en/headless
  2. code.claude.com/docs/en/best-practices
  3. typescriptlang.org/tsconfig
  4. typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html
  5. github.com/airbnb/ts-migrate
この記事を共有:XはてブLinkedIn