
問題は、最初の数週間ではない。Laravel側のDTOやResourceを変更したのに、フロントエンドの型定義が古いまま残り、ある日突然、undefined を前提にしていなかった画面が落ちる。しかも、バックエンドのテストは通り、フロントエンドの型チェックも通る。手動で二重管理している以上、こういう事故は「気をつける」だけでは消えない。
結局のところ、LaravelのAPI型定義をTypeScriptへ自動生成する仕組みは、型安全性を高めるためだけのものではない。仕様変更の伝達経路を短くし、誰かの記憶力に依存した運用を減らすための仕組みだ。
手動で型定義を管理すると、必ずどこかでずれる
たとえばLaravel側で、ユーザー情報を次のような形で返しているとする。
idは整数nameは文字列emailは文字列avatar_urlはnullを許容rolesは配列last_login_atは未ログインならnull
フロントエンドでは、このレスポンスをTypeScriptの型として別に定義することになる。最初は単純な作業に見える。
しかし、実際の案件では次のような変更が頻繁に入る。
avatar_urlを常に返すようにしたlast_login_atをログイン履歴の仕様変更で省略可能にしたrolesの文字列配列を、権限オブジェクトの配列に変更した- 一覧APIでは軽量な項目だけ返し、詳細APIではリレーションを追加した
- 認証状態によって返却項目が変わるようになった
バックエンドの変更担当者が、フロントエンドの型ファイルまで毎回更新してくれるなら問題はない。だが、炎上しかけた案件で「APIの変更をTypeScript側にも反映してください」と伝えたところ、「その型はどこにありますか」と返ってくることは珍しくない。型定義が複数のファイルに分散し、コピーされた古い型が残っているという罠がある。
さらに厄介なのが、null とオプショナルの違いだ。
avatar_url: string | nullこれはキー自体は存在し、値がnullになる可能性を表す。一方で、
avatar_url?: stringはキーが存在しない可能性を表す。API利用側にとって、この2つは似ているようで処理が違う。Laravelのレスポンス設計が曖昧なまま、フロントエンド側で雰囲気によって型を付けると、画面の条件分岐も曖昧になる。
型定義の二重管理は、仕様書を二冊持つことに似ている。最初は安心するが、更新されるのはだいたい片方だけだ。
Laravel APIとフロントエンドの型共有では、まず「どれを正とするか」を決めなければならない。PHPのDTOを正とするのか、OpenAPIスキーマを正とするのか、それともデータベースのModelを基準にするのか。この判断をせずに生成ツールだけ導入すると、型定義の自動生成が新しい混乱を作る。
まず選ぶべきは、型の生成元である
LaravelからTypeScriptの型定義を自動生成する方法はいくつかある。大きく分けると、次の4系統だ。
| 方式 | 主な生成元 | 向いている構成 | 注意点 |
|---|---|---|---|
| PHPクラス変換 | DTO、Enum、PHPクラス | Laravelとフロントエンドを近い距離で運用する構成 | APIの実際の返却形状は別途確認が必要 |
| DTO中心 | spatie/laravel-data などのDataオブジェクト | レスポンス仕様をDTOに集約する構成 | 動的なResource処理はそのまま表現しにくい |
| OpenAPI経由 | API仕様書、コードから生成したスキーマ | APIとフロントエンドを分離する構成 | スキーマ生成とコード生成の運用設計が必要 |
| Model変換 | Eloquent Model、DBスキーマ、リレーション | 管理画面やCRUD中心の構成 | APIレスポンスとDB構造は同じではない |
この表だけを見ると、Modelから生成すれば一番楽そうに見える。だが、Eloquent Modelはデータベースの都合であり、APIレスポンスはクライアントとの契約だ。ここを混同すると、内部カラムや不要なリレーションまでフロントエンドに露出する。
反対に、OpenAPIはきれいだが、導入した瞬間から理想的なスキーマ駆動開発になるわけではない。生成されたスキーマをCIで確認するのか、開発者が手動で更新するのか、破壊的変更をどう検知するのかまで決めなければ、立派なJSONファイルがリポジトリの片隅で眠ることになる。
個人的には、次のように整理すると判断しやすい。
- Laravelとフロントエンドが同じリポジトリにあり、DTOをきちんと使っているなら、PHPクラスやDataオブジェクトからの生成
- 外部クライアントや別チームのフロントエンドがあるなら、OpenAPIを契約の中心にする
- DBの列や管理画面用のデータを素早く参照したいなら、Model型生成を補助的に使う
- ルーティングやフォーム送信まで含めて連携したいなら、Laravel Wayfinderのような統合型の選択肢を検討する
「どのパッケージが一番高機能か」から考え始めると、だいたい迷子になる。必要なのは、現在のプロジェクトで変更の起点になっているものを見つけることだ。
Spatie TypeScript TransformerでPHPクラスを直接変換する
LaravelのPHPクラスをTypeScriptの型定義へ変換する方法として、Spatieのlaravel-typescript-transformerは比較的わかりやすい。
PHP側のクラスやDTO、Enumに#[TypeScript]アトリビュートを付け、php artisan typescript:transformを実行すると、[TypeScriptの型定義を出力できる。導入時のコマンドはcomposer require spatie](/articles/docker-composenonei/)/laravel-typescript-transformerだ。
この方式の良さは、型の定義場所がPHP側に寄ることだ。たとえば、権限を表すEnumをPHPで管理しているなら、そのEnumをフロントエンド用にもう一度書き直す必要がなくなる。値を追加したときに片方だけ更新する、というありがちな事故も減らせる。
Enumの共有は効果が大きい
API連携で意外と事故が多いのが、ステータスや権限の値だ。
PHP側で次のようなEnumを使っているとする。
draftpublishedarchived
フロントエンド側では、それを文字列のUnion型として扱うことになる。手動で書くなら、PHPのEnumに値を追加するたび、TypeScript側も修正しなければならない。変換対象に含めておけば、Enumの値を共有しやすくなる。
ただし、「変換できたから安全」と考えるのは少し早い。型が共有されても、APIが常にその型どおりの値を返すとは限らない。Resource内で条件分岐を入れていたり、アクセサで値を加工していたり、認証ユーザーによって返却項目を変えていたりする場合、PHPクラスの型と実際のJSONレスポンスが一致しないことがある。
ここが直接変換方式の弱点である。
PHPのクラスが、APIレスポンスそのものを表しているなら問題は少ない。だが、単なるドメインモデルや内部処理用のクラスを変換対象にすると、フロントエンドにとっては存在しない項目や、逆に必要なのに表現されていない項目が混ざる。
生成ファイルの扱いを先に決める
生成した.d.tsファイルをGit管理するかどうかも、早めに決めておいた方がいい。
Git管理する場合は、フロントエンド担当者がリポジトリを取得した直後から型を使える。差分レビューで「APIの型が変わった」ことも見えやすい。一方で、生成コマンドの実行忘れによって、PHP側と生成ファイルの差分が残る。
Git管理しない場合は、ビルド時に毎回生成する設計になる。こちらは同期漏れを減らしやすいが、フロントエンドだけを別環境で起動したいときに、PHP側の実行環境が必要になるという面倒が出る。
現場でよくある妥協案は、生成ファイルをGit管理しつつ、CIで再生成した結果に差分がないか確認する方法だ。CI/CD環境の具体的な手順はプロジェクト構成に左右されるが、少なくとも「生成を実行したかどうか」を人間の注意力だけに任せない方がいい。
直接変換方式が合うケース
Spatie TypeScript Transformerは、次のようなプロジェクトで使いやすい。
- LaravelとTypeScriptのコードが同じリポジトリにある
- PHPのDTOやEnumをAPI仕様の中心に置いている
- OpenAPIを別途運用するほどAPIが複雑ではない
- フロントエンドとバックエンドの変更担当者が近い
- 型生成のルールをPHP側に集約したい
逆に、APIを公開する予定がある、複数のクライアントが接続する、API仕様書を契約として残したい、といった場合はOpenAPI方式を検討した方が後で楽になる。
spatie/laravel-dataでDTOをレスポンスの中心に置く
APIの型を安定させたいなら、Eloquent Modelをそのまま返すより、DTOを経由させる方が扱いやすい。Modelには、リレーション、アクセサ、スコープ、内部用の属性が混ざる。便利な反面、外部へ返すデータの契約としては少々騒がしい。
spatie/laravel-dataを使うと、LaravelのDTOをDataオブジェクトとして定義できる。さらにTypeScript Transformerと組み合わせることで、PHP側のDTO定義からTypeScriptの型を生成しやすくなる。
たとえば、一覧画面向けのユーザー情報と詳細画面向けのユーザー情報を同じModelから直接返すのではなく、それぞれ別のDataクラスとして定義する。
UserListDataUserDetailDataUserProfileData
こうしておけば、「ユーザー型」という大きな一枚岩を作らずに済む。画面ごとにレスポンスの意味が違うのだから、型も分けた方が自然だ。何でも一つのUser型に押し込むと、項目が増え続け、最終的には「この画面で使わないプロパティ」が大量に付いた便利そうで不便な型になる。
Optionalとnullを区別する
laravel-dataを使うときに特に気を付けたいのが、LazyプロパティやOptionalプロパティの扱いだ。
APIでは、次の2つを区別したい場面がある。
- レスポンスに必ずキーはあるが、値がnull
- 条件によってキーそのものが存在しない
TypeScriptでは、前者がstring | null、後者がstring?に相当する。spatie/laravel-dataとTransformerの組み合わせでは、OptionalプロパティなどがTypeScript上のオプショナル型へ変換されるため、レスポンスの柔軟な形を表現しやすい。
これは、リレーションをLazyに読み込む構成で効果がある。ユーザー一覧ではプロフィール詳細を返さず、詳細画面だけでプロフィールを追加するようなAPIの場合、最初からすべての項目を必須にするとフロントエンドの型が現実と合わなくなる。
ただし、Optionalを便利だからという理由だけで多用するのは危険だ。何でも?にすれば型エラーは減る。しかし、その分だけフロントエンドは「あるかもしれないし、ないかもしれない」データを扱うことになる。型エラーを消しただけで、仕様の不確実さを解消したわけではない。
このあたりは、次のように決めると運用しやすい。
- 仕様上必ず返す項目は必須プロパティにする
- 値が空になるだけなら
nullを使う - リレーションを読み込んだ場合だけ存在する項目はOptionalまたは別DTOにする
- 認証状態で変わるレスポンスは、可能ならレスポンス型を分ける
- フロントエンドの都合でOptionalにせず、APIの実際の契約に合わせる
型定義は、エラーを黙らせるための注釈ではない。返ってくるデータの扱いにくさを、早い段階で見えるようにするためのものだ。
?を付ければ型安全になるわけではない。不確実なAPIを、不確実なまま正確に表現しているだけのケースもある。OpenAPIとScrambleでAPI契約を中心にする
Laravelとフロントエンドを別々に運用するなら、OpenAPIを経由した型生成が有力になる。
Laravel側では、dedoc/scrambleのようなツールを使って、ルーティングやコントローラー、リクエスト、レスポンスの情報からOpenAPIドキュメントを生成する。フロントエンド側では、そのスキーマをopenapi-typescriptやorvalに渡し、TypeScriptの型定義やAPIクライアントを生成する。
コマンドとしては、フロントエンド側でnpx openapi-typescriptを使う構成が代表的だ。実際の引数や出力先はプロジェクトによって変わるが、考え方は単純である。
1. LaravelのコードからAPI仕様を生成する
2. OpenAPIの内容をレビューする
3. OpenAPIからTypeScriptの型やクライアントを生成する
4. フロントエンドのビルドとテストで利用する
この方式の強みは、PHPの実装詳細ではなく、HTTP APIの契約を共有できることだ。Laravel以外のクライアントや、将来追加されるモバイルアプリにも同じ仕様を渡せる。
OpenAPI方式で起きる現実的な問題
一方で、OpenAPIを導入すると仕様書の管理が必要になる。ここを軽く見ると、別の二重管理が始まる。
Laravelのコード、生成されたOpenAPI、TypeScriptの型定義。この3つがそれぞれ別のタイミングで更新されると、せっかくの自動生成が形だけになる。API仕様を手書きで補足する箇所が多い場合は、そこが実装とずれる可能性もある。
特に、LaravelのResourceで動的に項目を追加している場合や、アクセサ、カスタムマクロ、複雑なリレーションを使っている場合は、自動推論の結果をそのまま信じない方がいい。動的なPHPコードは、静的なスキーマへ完全に変換するのが難しい。
OpenAPI方式を採用するなら、生成結果を一度確認する工程が必要になる。たとえば、次のような項目はレビュー対象にしたい。
- レスポンスのプロパティが必須になっているか
nullとOptionalが正しく区別されているか- 日付が文字列として扱われているか
- ページネーションの構造が一覧API間で統一されているか
- エラーレスポンスが成功レスポンスと同じように定義されているか
- Enumの値が実際のLaravel側と一致しているか
自動生成は、確認作業をゼロにするものではない。確認する対象を、ソースコードの細部から契約の差分へ移すものだ。
openapi-typescriptとorvalの使い分け
openapi-typescriptは、OpenAPIスキーマからTypeScriptの型定義を生成する用途に向いている。APIクライアントの実装はAxiosなどで自分たちの規約に合わせたい、という場合に扱いやすい。
一方、orvalは型だけでなくAPIクライアント生成まで含めた構成を取りやすい。React Queryなどのデータ取得ライブラリと組み合わせ、エンドポイントごとのフックを生成する運用も可能だ。
ただし、APIクライアントを自動生成すれば、設計上の問題まで解決するわけではない。生成された関数が増えすぎると、どの画面がどのAPIに依存しているのか見えにくくなる。キャッシュ戦略やエラー処理、認証トークンの更新などは、別途共通レイヤーを設計する必要がある。
型生成と通信処理の自動生成は、同じ問題ではない。ここを一緒に解決しようとすると、生成物にプロジェクトの都合を押し込みすぎることがある。
Eloquent ModelからTypeScriptを生成する方法は補助線として使う
LaravelのEloquent ModelからTypeScript型を生成するライブラリも存在する。fumeapp/modeltyperや7nohe/laravel-typegenなどがその例だ。データベーススキーマやリレーションを読み取り、Modelの構造をTypeScriptへマッピングする。
導入コマンドとしては、composer require fumeapp/modeltyperのような形になる。laravel-typegenにはLaravel 9以上を対応バージョンの目安とする構成もある。
Model型生成の魅力は、既存案件へ導入しやすいことだ。すでに大量のModelがあり、DTOの整備が追いついていない場合でも、データ構造のたたき台を作れる。管理画面やCRUD画面で、データベースの項目を参照しながらUIを作るときにも役立つ。
ただし、Modelの型とAPIレスポンスの型は同じではない。
たとえば、usersテーブルに次のようなカラムがあるとする。
passwordremember_tokencreated_atupdated_at- 内部管理用のフラグ
- 請求処理用の識別子
これらがModelの属性として存在していても、公開APIで返すとは限らない。むしろ返してはいけないものもある。Modelから型を生成し、そのままフロントエンドのAPI型として使うと、内部構造を公開契約のように扱うことになる。
リレーションも同じだ。hasManyやbelongsToを検出して型へ反映できても、実際のAPIで常にリレーションがロードされているとは限らない。Laravelのwith()の有無、Resource側の条件分岐、エンドポイントごとの設計によって返却形状は変わる。
Model型生成は、次のような用途に限定すると扱いやすい。
- 管理画面内部のデータ構造を補完する
- DTOを作る前の初期たたき台にする
- DBとコードの名前のずれを検出する
- リレーションの構造をフロントエンド開発者へ共有する
- API仕様ではなく、内部データの参照型として使う
APIの正式な契約を作るなら、DTOまたはOpenAPIを中心に置いた方が安全だ。Modelから生成した型を公開APIの唯一の型にするのは、力技だが、あとで境界線を引き直す作業が発生しやすい。
Laravel Wayfinderでルーティング連携まで扱う
Laravel 11、12の時代に向けた型安全な連携手段として、Laravel Wayfinderも登場している。ルーティングだけでなく、Model、Enum、バリデーションルールなど、フロントエンド向けの自動生成や補完を強化する方向のツールだ。
従来、フロントエンドからLaravelのルートを呼び出すときは、URLを文字列で書くことが多かった。
axios.get(`/users/${userId}`)この方法は簡単だが、ルート名を変更したり、パラメータ構造を変えたりすると、文字列を検索して修正することになる。URLが複数のコンポーネントに散っていれば、修正漏れが起きる。人間は文字列の検索に強そうで、意外と弱い。
Wayfinderのような仕組みを使えば、Laravel側のルート情報をフロントエンドで型付きの形で扱いやすくなる。ルートパラメータの補完や、変更時の検知に寄せられるのは大きい。
ただし、Wayfinderを採用すれば、API設計の問題まで消えるわけではない。ルートが型安全でも、レスポンスの意味が曖昧なら、フロントエンドは曖昧なデータを安全に受け取るだけになる。
また、導入時期や対応機能、既存構成との相性は確認が必要だ。新しいツールほど期待値が膨らみやすいが、既存のInertia.js構成なのか、LaravelとSPAを完全分離しているのかで、価値は変わる。
Wayfinderは、ルーティング、フォーム、型共有を一つの流れにまとめたいプロジェクトでは有力な候補になる。一方で、すでにOpenAPIを中心にAPI契約を運用しているなら、役割の重複が起きないかを見ておきたい。
Inertia.js構成とAPI分離構成では正解が違う
Laravelのフロントエンド連携では、Inertia.jsを使うのか、独立したSPAとしてAPI接続するのかで、型同期の考え方が変わる。
Inertia.js構成では、Laravelのルートと画面遷移が近い。コントローラーからページへ渡すPropsが重要になるため、DTOやDataオブジェクトをPropsの単位として整理すると、TypeScriptへ共有しやすい。
この場合、OpenAPIを導入してHTTP API全体を文書化するより、PHPのDTOやProps定義を直接変換する方が運用に合うことがある。ページ遷移とAPIクライアントを別々に設計する必要がないからだ。
一方、Vue.jsやReactを独立したSPAとして動かし、LaravelをAPIサーバーにするなら、OpenAPI方式の価値が上がる。APIを契約として切り出せるため、フロントエンドのビルド環境とLaravelの実行環境を分けやすい。
| 構成 | 型共有の中心 | 実践的な候補 |
|---|---|---|
| Inertia.js中心 | ページProps、DTO、Enum | TypeScript Transformer、Laravel Data |
| 同一リポジトリのSPA | DTO、OpenAPI | Laravel Data、Scramble、openapi-typescript |
| APIとSPAを別リポジトリで管理 | OpenAPI | Scramble、openapi-typescript、orval |
| 既存Model中心の管理画面 | Model、DBスキーマ | modeltyper、laravel-typegen |
| ルート呼び出しの型安全性を重視 | Laravelルート、フォーム、Model | Laravel Wayfinder |
ここで「どの方式が優れているか」と比較しても、あまり意味はない。プロジェクトの境界線が違えば、適切な生成元も変わる。
Inertia.jsにOpenAPIを無理に持ち込むと、画面PropsとAPI仕様の二つを管理することになる。反対に、複数クライアントがいるのにPHPクラスを直接TypeScriptへ変換すると、Laravelの内部構造にフロントエンドが強く依存する。
技術選定では、流行よりも境界線を見るべきだ。ここを飛ばすと、最新の道具で古い混乱を包むだけになる。
型生成をCIに組み込むときの落とし穴
自動同期を本当に機能させるには、開発者がコマンドを覚えていることではなく、変更時に同期漏れが検出されることが重要だ。
最低限、次の流れは作っておきたい。
1. Laravel側のDTO、Enum、Model、またはOpenAPI定義を変更する
2. 型生成コマンドを実行する
3. 生成されたTypeScriptの差分を確認する
4. フロントエンドの型チェックとテストを実行する
5. APIの破壊的変更があれば、画面側の修正を同じ変更単位で行う
たとえばSpatie TypeScript Transformerなら、php artisan typescript:transformを開発時だけでなく、検証環境でも実行できるようにしておく。OpenAPI方式なら、Laravel側でスキーマを生成し、フロントエンド側でopenapi-typescriptなどを実行する。
ただし、CIの設定を細かく書くこと自体が目的になってはいけない。プロジェクトごとのリポジトリ構造やデプロイ方式によって、最適な実行場所は違う。大切なのは、次のどちらかを明確にすることだ。
- 生成物をコミットし、差分をレビューする
- 生成物をコミットせず、ビルド時に必ず生成する
中途半端に両方を採用すると、生成物だけ古い状態で残ったり、ローカルでは動くのにCIでは生成できなかったりする。
もう一つ、破壊的変更の扱いも決めておきたい。レスポンス項目を削除する、必須項目を追加する、Enumの値を変更する、といった変更は、TypeScriptの型エラーとして現れることがある。これは面倒だが、型が仕事をしている証拠でもある。
型エラーを一括でanyに置き換えて通すのは簡単だ。炎上案件では、なぜかこの作業が「対応」と呼ばれることがある。しかし、それではAPI契約の変更を検知する仕組みを自分で壊している。
自動生成できないものを、無理に自動化しない
LaravelのAPIレスポンスには、自動変換が苦手なものもある。
- 実行時にだけ決まるアクセサ
- 認証ユーザーによって変わる項目
- 動的に追加されるMeta情報
- 条件によって形が変わるResource
- 複数のリレーションを組み合わせた集約結果
- 外部サービスのレスポンスを加工したデータ
これらまで完全に自動生成しようとすると、カスタムトランスフォーマーや補足定義が必要になる場合がある。動的なPHPコードを静的なTypeScriptへ100%変換するのは、そもそも難しい。
このときの現実的な対応は、手動定義を悪と決めつけないことだ。自動生成の対象を、安定して推論できる範囲に限定する。複雑なレスポンスだけは専用DTOやOpenAPIの補足定義で明示する。そうすれば、すべてを手書きするより管理範囲を減らせる。
自動化の境界線は、次のように引くとよい。
- Enumや単純なDTOは自動生成する
- DBの内部属性はModel型として補助的に生成する
- 公開APIのレスポンスはDTOまたはOpenAPIで契約化する
- 動的な項目は手動で補足し、生成処理に無理をさせない
- 生成物の正しさをテストや差分レビューで確認する
ここでの目的は、手作業をゼロにすることではない。手作業が必要な場所を、仕様上意味のある場所へ移すことだ。
結局のところ、最初に整えるべきはAPIの形
LaravelとTypeScriptの連携で、最初から複数の生成ツールを導入する必要はない。むしろ、レスポンスの形が固まっていない段階でツールを増やすと、生成された型を修正するための時間が増える。
まずは、APIの境界を整理する。
- このエンドポイントは一覧用か詳細用か
- nullになる項目と、省略される項目はどれか
- ページネーションの形式は統一されているか
- Enumの値はどこで管理するか
- Modelを直接返していないか
- 認証ユーザーによってレスポンスが変わらないか
- エラー時のレスポンスをどう扱うか
そのうえで、プロジェクトに合う方法を一つ選ぶ。
Laravelとフロントエンドが近く、DTO中心で進められるなら、Spatie TypeScript Transformerとspatie/laravel-dataの組み合わせは扱いやすい。APIを独立した契約として管理するなら、ScrambleなどでOpenAPIを生成し、openapi-typescriptやorvalへ渡す構成が現実的だ。
既存Modelの構造を素早く共有したい場合は、fumeapp/modeltyperや7nohe/laravel-typegenを補助線として使う。ルーティングやフォーム送信まで含めてLaravelとの接続を型安全に寄せたいなら、Laravel Wayfinderも候補になる。
ただし、ツールがAPIの設計を代わりに考えてくれるわけではない。型生成は、曖昧な仕様を自動で正しくする魔法ではなく、曖昧さを早めに露出させるための仕組みだ。
運用でカバーする部分は残る。生成コマンドをどこで実行するか、生成物をGit管理するか、変更を誰がレビューするかは、コードだけでは決まらない。一方で、Enumの重複やDTOの手書きコピー、ルートURLの文字列散在といった問題は、コードでかなり減らせる。
この線引きができれば、LaravelのAPI型定義をTypeScriptへ同期する仕組みは、過剰な設計ではなくなる。フロントエンドとバックエンドの間にある、あの微妙な「たぶん同じ」という感覚を、少しずつ減らせる。
華やかな自動化ではない。だが、夜に障害通知を見ながら古い型定義を探す時間を減らすには、こういう地味な仕組みが一番効く。