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
件数を取る
tsc -p tsconfig.strict.json --noEmitを、候補ファイルを足した状態で実行し、エラー件数を確認します。 - 2
小さいものから選ぶ
件数が少なく、他のファイルから呼ばれる側のファイルを先に選びます。
- 3
型を入れて通す
anyで埋めずに、実際の呼び出し元と使い方から型を決めて注釈します。
- 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 -20PHPStanの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 *)"
donenext-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を上げて型を直すと、その行のコメントが不要になり、未使用ディレクティブとして報告されます。次の流れです。
- 卒業させるファイルの
@ts-expect-errorの数を数える - 型を直して
tscを実行する - 「Unused '@ts-expect-error' directive」と報告された行のコメントを消す
- もう一度
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枚目の設定は不要になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。