2026.09.23 泊まれる場所4,223 立ち寄りスポット59,693 営業中を確認したキャンプ場1,005 note を読む

終了ボタンが押せない。どこからも読み込まれていないファイルが一つあった

tscもテストも通るのに、動かない。

日本進化シミュレーション第5回 / 全17回目次

ファイル分割が一段落した頃、ゲームを一通り触って確認していた。
タイトル画面へ戻る「終了」ボタンを押した。
何も起きない。

コンソールにエラーは出ていない。型チェックも通っている。テストも全部緑だった。
「動くはずのものが、静かに動かない」というのは、エラーが出るより厄介だ。
僕には、何を手がかりに調べればいいのか分からない。

ボタンのイベント登録自体が、一度も実行されていなかった

終了ボタンの処理はexit.tsというファイルにまとまっていた。
中身を読むと、コードは正しい。ボタンを取得して、クリックイベントを登録している。
ロジックとしては何も間違っていない。

おかしいのはそこじゃなかった。
このファイルを、どこからもimportしていなかった。

exit.tsは、他のファイルから関数を呼ばれることはなく、DOMのイベント登録は自分の中で完結しているタイプのモジュールだった。
だから「使われているように見えて、実は一度も読み込まれていない」という状態に気づきにくい。
importされていなければ、中身がどれだけ正しくても実行されない。当たり前のことなのに、分割作業の中では見落とした。

tscもVitestも、この種のバグは検出できない

型チェックは、書かれているコードの型が正しいかを見る。
テストは、呼び出した関数が正しく動くかを見る。
どちらも「そのファイルが、そもそも一度でも実行されるか」までは見てくれない。

コンパイルは通る。型も合う。ただ、一度も評価されないだけ。
このゲームにはreport.tsやphotomode.tsのような、同じ性質を持つファイルが他にもある。
「他ファイルから呼ばれる関数もあるが、DOMイベント登録は自分の中で完結している」という形のモジュールは、同じ穴に落ちる可能性がある。

孤児モジュールを機械的に見つける

一つ見つけて終わりにするのは怖かったので、全部のファイルを機械的にチェックすることにした。

for f in src/*.ts; do  name=$(basename "$f" .ts)  case "$name" in main|global|*.test) continue;; esac  hits=$(grep -lE "from \"\./$name\"|^import \"\./$name\";" src/*.ts 2>/dev/null | grep -v "^src/$name.ts$" | wc -l)  if [ "$hits" -eq 0 ]; then echo "ORPHAN: $name.ts"; fidone

どのファイルからも参照されていないファイル名を、ORPHANとして吐き出すだけの単純なワンライナーだ。
これを回して、exit.tsが本当に孤立していたことを確認し、main.tsへimport "./exit";を一行足して直した。

コンパイルが通ることと、動くことは別の話

この一件で覚えたのは、「型チェックとテストが両方通っている」という安心感を、そのまま「機能が動く」に読み替えてはいけないということだった。

ファイルを分割するという作業そのものが、新しい種類のバグを持ち込む。
単一ファイルの時代には存在しなかった「importし忘れ」というミスは、分割した本人にしか気づけない。
以来、新しいファイルを追加したりimportを張り替えたりした後は、この孤児チェックを機能テストとは別に必ず回すようにしている。

ゲームを1本作るときの進め方を、noteにまとめています(書いているのは私です)。
noteで読む

まる子パパ

まる子パパ

会社員。受託開発のエンジニアを15年以上続けていて、その前は寿司職人・長距離運転手・農業。 技術ブログは、キャンプの合間に踏んだバグと、AI・Web開発の運用の失敗を書いています。 このサイト自体が、開発に携わっているCMS「コンテナ」の稼働中の実例です。 このサイトについて / 小さな自動化の相談

—

ほかにも書いています

受託・note・スタンプ

※受託の相談とnoteは、運営者が個人で受けている・書いているものです。

技術ブログ一覧へ戻る