PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

AIエージェントによる段階的・安全なTypeScript移行方法

こんにちは、PLAY CLOUD本部 技術推進室の市川です。

PLAY CLOUDでは現在、プロダクトの管理画面について、稼働させたまま TypeScript に置き換える 作業を進めています。

JavaScript から TypeScript への移行は、型による不具合の早期検出、IDE 補完の精度向上、リファクタリングの安全性、新メンバーのオンボーディング高速化など、技術的なメリットが非常に大きい施策です。 一方で、ある程度の規模になったプロダクトでこれをやり切ろうとすると、人月単位の時間とコストがかかる こと、機能開発リソースと完全に競合する こと、直接的な利益に繋がることがわかりづらい ことなどから、なかなか踏み切れないまま放置されてしまうケースは少なくないのではないでしょうか。

そこで本記事では、Claude Code を「規約に沿って動く TS 化専門の同僚」として運用することで、この TS 移行をどのように安全かつ持続的に進めているかを解説したいと思います。 具体的には、.claude/ 配下の設定(rules / agents / skills / hooks)の作り方、test-first フローによる安全装置、SDK 型を一次情報源にすることで AI のハルシネーションを抑える工夫など、実際の運用知見を中心にお伝えします。 またこちらの内容はTSKaigi 2026で採択され登壇する内容になります。

2026.tskaigi.org

Claude Code を使った TS 移行の全体像

Claude Code はリポジトリ内の .claude/ 配下を 常時セッションに読み込んで動く AI コーディング環境です。 ChatGPT などで毎回プロンプトに規約を貼り付ける運用と違い、.claude/rules/ に規約 Markdown を置いておけば、Claude は 毎セッション同じルールに従って判断 します。

今回の TS 移行で採用している方針は次の通りです。

項目 方針
互換性 allowJs: true で JS / TS を共存させ、依存の少ないファイル(葉)から徐々に変換
自動化 一括変換スクリプトは作らず、AI が yarn tsc && yarn lint && yarn test を回して都度判断
精度向上の工夫 型情報を推測する時に API の SDK の型情報を一次情報源にする
安全装置 test-first: 変換前にテストを書き、JS で 🟢green → TS 化 → 🟢green を確認

これらをすべて .claude/ 配下に落とし込み、AI に 毎セッション再現可能なルーチン として実行させることが重要となってきます。 この移行では特に API の SDK の型情報を一次情報源にする こと、test-first で変換していく ことがポイントとなっています。 次から、それらの仕組みについて具体的に説明していきます。

SDK 型を一次情報源にする

AI に TS 移行を任せる際、最大の落とし穴になるのが 型の推測 です。 SDK 型を AI に明示的に読ませることで、このハルシネーションを構造的に抑えられます。

SDK の型の生成・利用方法

TS 移行したプロダクトでは SDK を OpenAPI から自動生成し、GitHub Packages で npm パッケージとして配布し、利用者側はそれを npm install して用いるという運用をしています。 このプロダクトでは API スキーマを以下のパイプラインで管理しています。

[ OpenAPI 仕様 (openapi.yaml) ]   ← チームルールとして「先に書く」
    │  API スキーマの形式記述
    ├───────────────┐
    ▼                         ▼
[ API 実装 ]      [ SDK 自動生成 (openapi-generator) ]
  express-openapi で           │  TypeScript 型 + クライアントが吐き出される
  読み込み・リクエスト            ▼
  バリデーション           [ npm パッケージ (@example/console-api-sdk) ]
                               │
                               ▼
                         [ フロントエンド ]  ← yarn add で取り込み

サーバ側では express-openapi を使っており、起動時に openapi.yaml を読み込んで、各エンドポイントに対するリクエストバリデーションを自動で適用します。 express-openapi 自体は「先に OpenAPI を書くこと」を強制するわけではありません(後から定義を足してもよい)。ただしスキーマ定義に書いたパスとオペレーションだけがルートとして有効になる ため、定義のないエンドポイントは実装しても呼べません。 そのため運用ルールとして 「OpenAPI を先に書いてからサーバ実装に入る」 をチームで徹底することで、結果として OpenAPI が常に最新の一次情報源として保たれる、という運用にしています。

このパイプラインがもたらす性質は次の通りです。

  • OpenAPI が常に一次情報源: 「先に書く」運用+実装はスキーマ越しでしか呼べない仕組みで、後付けの食い違いが残りにくい。
  • サーバ実装とスキーマが機械的に一致: 手書きの解釈が入らない。SDK 型 ≒ 実 API スキーマ。
  • リクエストバリデーションも自動: スキーマ違反のリクエストは API のハンドラに到達する前に弾かれる。
  • スキーマ更新 = npm install で型が降ってくる: API 側の変更箇所がすべて型エラーで通知される。
  • union 網羅性をその場で検証可能: CategoryType = "news" | "sports" | ... のような形式言語として効く。

つまり OpenAPI が 「実装の起点」かつ「クライアント側の型の一次情報源」 という二重の役割を果たしているのが、この仕組みのキモです。

AI に SDK の情報を認識させる

AI に SDK の情報を認識させる方法は非常に簡単です。 Claude Code の rules のファイルとして .claude/rules/typescript-style.md という TypeScript の型付けの作法を書いたドキュメントを用意したのですが、 そこに次のような 1 行を入れておけば、AI は変換のたびに対応する SDK 型を grep で探すルーチンに入ります。

## API 型は SDK 由来必須

- API リクエスト / レスポンス / ドメインモデルの型は
  **`@example/console-api-sdk` から import** する
- 自前で再定義しない

これだけで、AI 間 / セッション間で同じ関数に違う型が付く という再現性の問題が消えます。 セッションが切れて別の Claude が引き継いだとしても、同じ規約が再適用されるためです。

「素のまま使う」と「派生させる」を分ける

ただし、SDK 型を そのまま すべての箇所に流すのは正解ではありません。 画面入力やフォーム値は API ペイロードとは別物だからです。

用途 型の選び方 理由
API レスポンス SDK 型そのまま 完全に一致するため
API リクエスト送信 SDK 型 or Partial<SDK型> 境界点
フォーム値・画面状態 自前 or Pick<SDK, ...> 画面と API は別概念
SDK と概念が異なる 自前 + 理由コメント 例: SDK で boolean、画面では 0/1 number

実際、あるドメインモデルでは create と update でペイロード型が異なるため、Partial<ResourceCreatePayload>Partial<ResourceUpdatePayload> を作り分けて使い分けています。 原則は 「境界では SDK 型・画面内では派生」 です。

test-first フローで「振る舞いを壊さない」

TS 移行で最も恐れるべきは 「型をつけたつもりが、ロジックを書き換えてしまっていた」 というケースです。 TS 化されていないシステムは、テストが書かれていないことも多いのではないでしょうか? このプロジェクトも元々テストが存在しない状態だったため、変換時に以下に記載する test-first フロー を必ず通すことにしました。 TS 化と同時にテストも AI に書かせることでシステムとしての堅牢性が高まります。

標準フロー

  1. 対象ファイルの挙動を観察してテストを書くまだ .js のまま
  2. yarn test で 🟢green を確認 → テスト自体が正しいことを保証
  3. git mv.js.ts にリネームし、その時点で一度コミット(履歴を辿るため必須)
  4. 型を付ける(SDK 型優先、なければ自前 + 理由コメント)
  5. yarn tsc && yarn lint && yarn test をすべて 🟢green に

このフローで処理するように指示をしておくことで、ある種のテスト駆動開発のような形で TS 化を進めていきます。

.claude/ の構成:4 種類のファイルにすべて落とし込む

ここまでの方針を、.claude/ 配下に以下のように整理しています。

.claude/
├── rules/                  # プロジェクト規約の Markdown 群
│   ├── typescript-migration.md   # HARD RULE 12 個
│   ├── typescript-style.md       # 型付け作法
│   ├── testing.md                # テスト方針 + 妥協ライン
│   ├── imports.md                # パスエイリアス運用
│   └── ui-styling.md             # CSS 命名規約
│
├── agents/                 # サブエージェント (役割 + モデル)
│   ├── ts-migrator.md            (opus)
│   ├── test-writer.md            (sonnet)
│   ├── ts-build-doctor.md        (opus)
│   ├── props-typer.md            (sonnet)
│   └── migration-scout.md        (haiku)
│
├── skills/                 # スラッシュコマンド化されたワークフロー
│   ├── ts-status/        # 進捗集計
│   ├── ts-check/         # yarn tsc/lint/test 一括
│   ├── ts-migrate-file/  # ファイル単位 test-first 変換
│   └── ts-migrate-dir/   # ディレクトリまとめて
│
└── settings.json           # hooks など (Stop イベントで通知)

以下で重要なものに対して詳細と、それぞれの役割・効果を見ていきます。

rules/ — 不変条件を Markdown で明文化

typescript-migration.md の中心は以下の HARD RULE 12 個です。

1. 新規ファイルは .ts/.tsx のみ
2. 1 ファイル変換 = 2 コミット(① git mv だけ → ② 型付け)
3. テストを先に書く(test-first)
4. 変換後は yarn tsc && yarn lint && yarn test がすべて 🟢green
5. 依存先 (import 元) から先に変換(葉から進める)
6. any 禁止
7. @ts-nocheck / @ts-ignore / @ts-expect-error の新規追加禁止
8. API 型は SDK の型を参照する。極力自前で定義しない
9. 既存の振る舞いを変えない(型付けと等価リファクタを混ぜない)
10. ロジック・構造は極力変更しない
11. ファイルリネームは git mv + 一度コミット する(履歴を辿れるように)
12. React component の Props / State 型は export する

特に重要なのはこれまで話してきた test-first で行う 3, 4 のルール、精度を上げるのに用いている 8 の SDK の型を参照するルールですが、 9, 10 のルールも変換していると地味に重要でした。 変換時に AI はよしなにリファクタリングをしてロジックの書き方を最適化する変更などをします。 それ自体は悪いことではないのですが、TS 化という目的においては型をつける以外の差分が増え、レビュー時にノイズに感じてしまいます。 また 100% 正しい変換をするとも限らないので、それによってバグを発生させてしまう可能性もあります。 11 のルールも、書いておかないとファイルを削除して新規に ts ファイルを作るという挙動をするので、差分を追うためには必要なルールです。 ただし git mv だけでは不十分で、リネームと中身の書き換えを同じコミットに混ぜると Git のリネーム検出が外れ、結局「削除+新規追加」として記録されてしまいます。 そのため 2 のルールで 「リネームだけのコミット」と「型付けのコミット」を分ける ことを明記しています。これにより git log --follow で履歴をきれいに辿れるようになります。

agents/ — 役割ごとに切り出してモデルを使い分ける

Claude Code は サブエージェント を作って機能を分担できます。 モデルを役割に応じて使い分けることで、コスト効率と品質を両立できます。

Agent 役割 モデル
ts-migrator 単一ファイルを test-first フローで変換 Opus
ts-build-doctor tsc のビルドエラーを分析・修正 Opus
test-writer 既存挙動を観察して Vitest テストを書く Sonnet
props-typer コンポーネント Props を呼び出し元から逆算 Sonnet
migration-scout 依存グラフから次の変換対象を提案(読み取り専用) Haiku

ポイントは 思考の深さに合わせて課金単位を変える ことです。

  • Opus = 型を考えて修正する系
  • Sonnet = 定型作業系
  • Haiku = スキャン・集計系

「次にどのファイルを変換すべきか」のような依存グラフ走査は Haiku で十分ですし、「ビルドエラーが多ファイルにまたがって連鎖した」ような複雑な解析は Opus に任せます。

skills/ — 頻出ワークフローをスラッシュコマンド化

頻繁に使う作業をスラッシュコマンドにしておくと、誰でも同じ手順を再現できます。 たとえば /ts-status を打つと、ディレクトリ別の .js / .ts 比率と全体の TS 率がすぐ出ます。 /ts-migrate-file path/to/Foo.js と打てば、test-first フローを最後まで自動で回してくれます。

属人化しないので、チームの別メンバーが続きを引き受けられる のも大きな利点です。

実際の挙動

ここまで解説してきた内容の設定を行い、実際に Claude Code がどのような挙動をするかを示したターミナルのキャプチャになります。 指示としては、「TypeScript 化を進めて」と入力するだけです。

① 変換対象を依存の少ないものから抽出.
② SDK の情報を確認.

③ テストの作成、green であることの確認.
④ git mv で対象ファイルを .js ファイルから .ts ファイルに変換.
⑤ 型情報をつける修正を実施.
⑥ TS 化完了後に再度テストの実施.

ルールに記載されたことを守りつつ、SDK の一次情報を参照して、test-first で TS 化していることが分かります。

おわりに

今回は、人月単位の時間とコストがかかるために踏み切れずに放置されがちな TypeScript 移行を、Claude Code を「規約に沿って動く TS 化専門の同僚」として運用する ことで安全かつ持続的に進める方法を解説しました。 ポイントは大きく次の 2 つでした。

  • SDK の型を一次情報源にすることで、AI の型推測によるハルシネーションを構造的に抑える
  • test-first フローを必ず通すことで、変換前後の振る舞いの等価性を担保しつつ、結果としてテスト資産も積み上がりシステム全体の堅牢性が高まる

そして、これらを .claude/ 配下の rules / agents / skills / settings.json に落とし込み、AI に 毎セッション再現可能なルーチン として実行させているのが本記事の肝です。 特に rules による HARD RULE の明文化、思考の深さに応じたエージェントとモデルの使い分け、頻出フローのスラッシュコマンド化により、TS 移行という地味で長丁場な作業を 属人化せず、止まらず 進められるようになりました。 規約を AI が常時読む形にしておけば、

  • セッションが切れても同じ判断が再現される
  • チームの別メンバーが続きを引き受けられる
  • 「一気にやらない」「中途半端で止まらない」が構造的に保たれる

というメリットが得られます。 重要なのは「AI に任せる」ことそのものではなく、「AI が守れる規約を先に整える」 ことだということが、この移行を通じて得られた一番の学びでした。 この内容が、レガシーコードを抱えながら型安全性を高めていきたい、リファクタリングを安全に AI に任せたい、と考えているエンジニアの方の参考になればと思います。