ブログに戻る

JavaScript プロジェクトに TypeScript を後から入れたときの具体的なバグ

TypeScriptrefactoring

ヒーロー画像

結論

kimura512.dev は JavaScript から始め、後から TypeScript を strict 寄りに上げました。後付けの型付けで得られるものは、新機能ではなく「実行時まで潜んでいた前提の発見」です。ビルドが赤の間は本番デプロイを止め、型エラーを直すコミットと機能追加のコミットを分けました。

1. 翻訳キー

多言語の翻訳は translations.ts に集約していますが、存在しない言語キーは実行時まで気づかれません。一部の言語だけ文言が抜けていても、画面上は空欄や別言語の文字として表示されるだけで、エラーになりません。Record<Language, string> の形に寄せ、足りない言語はビルドで落とすようにしました。翻訳漏れは「表示の不具合」ではなく「型エラー」として先に捕まえられます。

2. Markdown の href

ブログ本文の Markdown リンクで、href が optional なのに無条件利用していた箇所がありました。リンクが無いケースを考慮せずに文字列を扱っていたため、型を付けて初めて問題に気づきました。内部リンクと外部リンクの判定も型で明示し、判定ロジックを一か所にまとめました。見た目は変えず、データ境界だけ固めるのが後付け TS のコツです。

3. 日付文字列

投稿データの一部で日付形式がずれ、表示時に Invalid Date になった箇所がありました。BlogPost 型で ISO 形式を保証し、形式が違うデータはビルドの段階で検出できるようにしました。日付はユーザーに見える部分なので、実行時エラーより先にビルドで落とした方が安全です。

4. 外部 API の as キャスト

API ルートの一部を zod で検証し、不正 JSON は 400 で返すようにしました。as でキャストしていた箇所は、レスポンスが想定と違っても型が合ってしまうため危険です。境界で実行時に検証することで、想定外のデータが内部に入り込むのを防いでいます。

5. React 19 と useRef

React 19 まわりで、useRef の初期値省略により null 合体が必要になった箇所を修正しました。children も ReactNode に統一しています。型定義が古いままだと、ビルドは通っても実行時に意図しない値が入ることがあります。依存ライブラリのバージョンに合わせて型の前提を見直す必要がありました。

6. デプロイ前

vinext build が型エラーを拾うため、push 前に必ず build を通します。ローカル dev では動いても、本番ビルドの型チェックで落ちるケースがあります。E2E が薄い個人サイトでは、build が最後の防波堤になっています。

7. コンポーネントの props

GlassCard の variant を union 型にしたことで、存在しない variant 指定がビルドで落ちるようになりました。許される値を型で列挙すると、誤字や古い指定が残りにくくなります。デザインを変えずに型だけ足す方針を守りました。

8. テストの代替

型エラーを直すコミットは機能変更と分け、レビューしやすくしています。見た目の差分はゼロに近い状態で strict を上げました。見た目が変わると不具合と誤解されやすいので、リファクタと機能追加は別 PR に分けています。

翻訳やブログデータを直すときは、まず型を通してから UI を触る順番にしています。コンポーネント側で as を足して誤魔化すと、同じ問題が別の画面で再発します。strict を上げたあとも、デザインの差分はゼロに近い状態を保ち、レビューで「見た目が変わったのか型だけか」を切り分けやすくしました。

まとめ

データが入ってくる境界から固めると、UI より先に安定します。画面側で型を合わせるより、投稿データや API の入り口で形を保証する方が、修正量が少なく済みました。

補足

Q. strict はいきなり?

noImplicitAny から週1ルール追加で上げました。一度に全部上げると修正量が多すぎて作業が止まるため、ルールを一つずつ足しています。

Q. any はゼロ?

外部 SDK だけ unknown 経由で残しています。any を残す場合は範囲を一か所に閉じ込め、他へ広がらないようにしています。

型エラーが出たファイルは、まず posts.ts や翻訳 JSON から直すかを決めます。UI コンポーネントが長いほど、原因はデータ側に寄っていることが多いです。