
Laravel側では、Eloquent ModelやAPI Resource、DTO、Form Requestがデータの形を決める。一方、TypeScript側では、手書きのinterfaceや型エイリアスを用意する。最初は数個の画面で済む。しかし機能が増えると、片方だけプロパティ名が変わる。null許容がずれる。Enumの値が古いまま残る。レスポンスの構造が変わったのに、フロントエンドの型定義だけが更新されない。
コンパイルは通る。画面も一部は動く。けれど、特定の条件でだけ表示が崩れる。ユーザーが最初に見つけ、開発者が後から追いかける。厄介なタイプの不具合だ。
この問題に対して、LaravelとTypeScriptの型定義を自動生成する手法がある。Modelから直接抽出する方法、DTOから生成する方法、OpenAPIを経由する方法、Laravelのルートやバリデーション規則まで扱う公式エコシステムを使う方法。選択肢は増えた。
ただし、自動生成という言葉だけを見て導入すると、別の問題が生まれる。どの層を正とするのか。生成された型を誰が使うのか。APIの契約をどこで管理するのか。ここを決めないままツールを入れても、型定義のファイルが増えるだけだ。
この記事では、LaravelとTypeScriptの型連携を「どのツールが一番優れているか」ではなく、「自分の構成に対して、どの境界を自動化するべきか」という視点で整理する。
型定義の自動生成は、開発速度より先に認識のズレを減らす
LaravelとTypeScriptの連携で起きる問題は、単純な作業量だけではない。
手書きの型定義は、数分で書ける。Modelの属性をinterfaceに写し、APIレスポンスの型を定義する。初期段階では、自動生成の仕組みを作るより速いこともある。
問題は、その型が時間の経過に耐えないことだ。
たとえば、Laravel側のユーザー情報にavatar_urlを追加したとする。API Resourceは更新したが、TypeScriptの型定義を更新し忘れる。あるいは、Laravel側ではemail_verified_atがnullになり得るのに、TypeScript側では必須のstringとして扱っている。
このズレは、単なる記述ミスではない。バックエンドとフロントエンドが、別々の仕様を見ながら開発している状態だ。
型定義の自動生成で狙うべきなのは、コードを書く時間の削減だけではない。仕様変更が起きたとき、片方の変更をもう片方に伝える仕組みを作ること。つまり、フロントエンドとバックエンドの間にある認識のズレを、早い段階で検出することだ。
私がこのテーマで重視しているのは、生成された型の美しさではない。変更に対して、どれだけ早く壊れ方が分かるかという検証可能性だ。
- Modelの属性変更が、TypeScript側の差分として見えるか
- APIレスポンスの変更を、フロントエンドのビルドで検知できるか
- Enumの追加や削除が、利用箇所に伝播するか
- 開発者が生成ルールを理解し、継続的に運用できるか
- 生成された型が、実際のレスポンスをどの程度正確に表しているか
ここを見ないと、「型安全になったはずなのに、実際のAPI通信では不具合が起きる」という状態になる。
型定義の自動生成は、型を書く作業を消す仕組みではない。どのデータを仕様として信頼するかを、チームで固定する仕組みだ。
Eloquent Modelから直接型を抽出するアプローチ
最初に検討しやすいのが、LaravelのEloquent ModelからTypeScriptのinterfaceを生成する方法だ。
この方式の魅力は分かりやすい。すでにModelにテーブル属性やリレーションが定義されているなら、そこを起点に型を作れる。フロントエンドで利用するデータの多くが、Modelの構造とほぼ一致しているプロジェクトでは導入効果が出やすい。
代表的な選択肢として、fumeapp/modeltyperがある。Eloquent ModelからTypeScriptのinterfaceを自動生成し、テーブル属性やリレーション、特殊メソッドにも対応するライブラリだ。
また、7nohe/laravel-typegenもLaravelのModel、Relation、Enum、さらにルーティングに関する型を生成できる。Laravel 9以上に対応し、Node.jsやnpmパッケージとして利用する構成になる。
Model直結型のメリット
Modelから型を生成する方式は、次のようなケースで扱いやすい。
1. 管理画面や社内ツールなど、Modelのデータを比較的そのまま表示する場合
API Resourceで大きく変形せず、属性を一覧や詳細画面に渡す構成なら、ModelとTypeScriptの対応関係が理解しやすい。
2. Laravelとフロントエンドが同じリポジトリにある場合
生成コマンドを開発フローやビルドに組み込みやすい。Model変更後に型生成を実行し、差分を確認する流れも作りやすい。
3. まず手書きの型定義を減らしたい場合
DTOやOpenAPIを導入するほどAPI設計を整理できていない段階でも、重複記述を減らせる可能性がある。
特に、Modelの属性と画面表示の距離が近いプロダクトでは有効だ。個人開発では、管理画面の検索条件や一覧テーブルを増やしていくうちに、似たような型を複数箇所へ書いてしまう。そこをModel起点でまとめられるだけでも、変更時の確認箇所は減る。
Model直結型の落とし穴
一方で、Eloquent Modelは、そのまま外部公開APIの契約になるとは限らない。
データベース上には存在するが、APIでは返さない属性がある。内部管理用のフラグ、監査情報、権限に関する値などだ。反対に、API側で整形した表示名や集計値、URL、状態ラベルを追加することもある。
つまり、次の2つは似ているようで別物だ。
- データベースやModelが持っているデータの形
- クライアントに公開するAPIレスポンスの形
Modelから生成した型を、APIレスポンスの型としてそのまま使うと、この境界が曖昧になる。Laravel側でhidden属性を設定している場合や、Resourceで項目を変形している場合、生成されたinterfaceが実際のレスポンスを正確に表すとは限らない。
リレーションも同じだ。常にロードされるリレーションなのか、条件によって含まれるのか。postsが配列として存在するのか、未ロード時には存在しないのか。Modelの構造を抽出できても、API通信時の条件まで自動的に完全反映できるとは限らない。
ここでの仮説は、「Modelを正として型連携すれば、すべてのAPI型定義を解決できる」ではない。
より現実的には、Model直結型は次の領域に置く。
- 管理画面の内部データ
- Inertia.jsなど、Laravelとフロントエンドが密接に連携する画面
- API Resourceによる変形が少ない読み取り処理
- Modelの属性やEnumを、補助的な共有型として利用するケース
外部クライアント向けの公開APIでは、別の契約層を設けたほうが安全だ。
DTOとSpatieパッケージで「公開するデータ」を型にする
ModelとAPIレスポンスの間に距離があるなら、DTOを起点にする方法が適している。
DTOは、どのデータを、どの形で外部へ渡すのかを明示するための構造だ。Modelの全属性を公開するのではなく、画面やユースケースに必要なデータだけを定義できる。
この考え方は、型安全のためだけにあるものではない。APIの境界を整理するためにある。
たとえば、ユーザー情報を返す処理で、データベースには次のような属性があるとする。
- 内部管理用の識別情報
- ログイン状態に関する情報
- 表示名
- プロフィール画像
- 登録日時
- 権限情報
そのままModelから型を生成すると、フロントエンドが必要としない項目まで型に含まれる可能性がある。DTOを使えば、画面に公開する項目だけを明示できる。返さないものは、最初からTypeScriptの型にも登場しない。
Spatieのlaravel-typescript-transformer、およびtypescript-transformerは、PHPのデータオブジェクトやlaravel-dataのDTOクラスからTypeScriptの型定義を自動生成できる。
ここで重要なのは、DTOを作ること自体を負担と捉えすぎないことだ。
確かに、ModelとDTOの両方を定義する必要がある。小さな画面では二重管理に見える。しかし、その二重化には意味がある。Modelはデータベースやドメインの都合を表し、DTOはクライアントとの契約を表す。役割が違う。
DTO起点で型連携するメリット
DTO方式を採用すると、型の責任範囲が明確になる。
- データベースの変更とAPI契約の変更を切り分けられる
- 外部へ公開する項目を意識的に選べる
- null許容や配列構造をDTO側で表現できる
- 複数のModelを組み合わせたレスポンスを扱いやすい
- TypeScript側に公開してよいデータだけを生成できる
特に、同じModelを複数の画面で異なる形にして返す場合に強い。
一覧画面では軽量なDTO、詳細画面では関連情報を含むDTO、編集画面では入力用DTO。これらを分けることで、「User型」という一つのinterfaceにあらゆる意味を詰め込む必要がなくなる。
型名に意味を持たせられる点も大きい。Userだけでは、その型が一覧用なのか、認証ユーザー用なのか、管理者向け詳細用なのか分からない。DTOを起点にすれば、用途ごとの型としてTypeScriptへ出力できる。
DTO方式で増える運用コスト
もちろん、DTOにも弱点がある。
まず、定義すべきコードが増える。個人開発では、最初からすべてをDTO化すると、画面を一つ追加するたびに作業が重く感じられることがある。
また、生成されたTypeScript型を使っていても、実際に返しているLaravel側の処理がDTOを経由していなければ意味がない。型だけを生成し、ControllerやResourceが別の配列を返すような運用では、契約が分裂する。
そこで、DTOを採用するなら、生成コマンドだけではなくデータの流れも揃える必要がある。
1. ControllerやActionが受け取る入力を定義する
2. バリデーション済みデータをDTOへ変換する
3. DTOをもとにレスポンスを返す
4. DTOからTypeScriptの型を生成する
5. フロントエンドで生成型を利用する
この流れができて初めて、DTOがAPI契約の中心になる。
Spatieの仕組みは、DTOを明示的に設計したいプロジェクトと相性がいい。一方、既存コードがModelや配列中心で動いている場合は、一部の画面から段階的に導入するほうが現実的だ。
DTOはコードを増やす。しかし、公開データの責任者を増やさない。ModelとAPIの境界が曖昧なプロダクトほど、この差が効いてくる。
OpenAPIを中心に据える、APIスキーマ駆動の型連携
Laravelとフロントエンドを別リポジトリで運用している。あるいは、将来的にモバイルアプリや外部クライアントからAPIを利用する予定がある。その場合は、ModelやDTOではなく、APIスキーマを中心に型連携を設計する方法がある。
この方式では、LaravelのAPIをOpenAPIドキュメントとして表現し、そのスキーマからTypeScriptの型定義を生成する。
dedoc/scrambleのようなツールを使えば、Laravelのルーティングや処理からOpenAPIドキュメントをアノテーションなしで生成できる。生成されたドキュメントの例として/docs/api.jsonがあり、それをopenapi-typescriptでTypeScriptの型定義へ変換する流れだ。
型の起点がModelではなく、HTTP APIの仕様になる。ここが大きな違いだ。
OpenAPI方式が向いている構成
OpenAPIを採用する価値が高いのは、次のような状況だ。
- Laravelとフロントエンドを分離リポジトリで管理している
- APIを複数のクライアントが利用する
- APIのURL、HTTPメソッド、リクエスト、レスポンスを契約として管理したい
- フロントエンドの型だけでなく、APIドキュメントも必要
- 将来的にSDKや通信クライアントを整備したい
Modelから型を生成する方式では、HTTPレベルの情報が不足しやすい。どのエンドポイントが存在するのか、どのパラメータを受け取るのか、エラー時に何を返すのか。APIクライアントに必要なのは、データの型だけではない。
OpenAPIなら、データ構造だけでなくAPIの入口と出口を一つのスキーマにまとめられる。
OpenAPIの導入で気をつける境界
ただし、OpenAPIを導入しただけで、APIの実装と仕様が自動的に一致するわけではない。
Laravelのコードを解析してOpenAPIを生成する場合、どの情報をどこまで正確に抽出できるかは、実装の書き方やパッケージの対応範囲に左右される。特殊なレスポンス処理や、条件によって構造が変わるAPIでは、生成結果を確認する必要がある。
また、OpenAPIの型をTypeScriptへ変換しただけでは、通信処理の使い勝手までは決まらない。
たとえば、型定義が生成されても、Axiosで次のような処理を毎回手書きしていれば、エンドポイント名やパラメータの指定ミスは残る。
- URLの入力
- HTTPメソッドの指定
- クエリパラメータの組み立て
- リクエストボディの作成
- レスポンス型の指定
- エラー処理
ここで、型定義と通信処理をどこまで自動化するかという次の仮説が出てくる。型だけを生成するのか。APIクライアントまで生成するのか。あるいは、型はOpenAPI、通信処理は既存のAxiosラッパーで管理するのか。
私は、最初からすべてを自動化するより、まずAPIスキーマをCIで生成・確認する運用から始めるほうが安全だと考えている。
APIの変更時にOpenAPI JSONの差分が出る。TypeScript側で型生成を実行する。フロントエンドのビルドやテストで影響箇所を確認する。この流れだけでも、手書き型の更新漏れを減らせる。
Model、DTO、OpenAPIの役割を混同しない
ここで一度、3つのアプローチを整理する。
| 起点 | 主に表現するもの | 得意なケース | 注意点 |
|---|---|---|---|
| Eloquent Model | データベースやModelの属性、リレーション | Modelと画面データが近い構成 | 実際のAPIレスポンスと一致しない場合がある |
| DTO | 公開するデータの構造、ユースケース単位のデータ | APIの境界を明示したい構成 | DTOを実際の処理で必ず利用する必要がある |
| OpenAPI | HTTP APIの契約、リクエスト、レスポンス | 分離フロントエンド、複数クライアント | 生成された仕様を実装と照合する運用が必要 |
この表で分かるように、優劣ではなく、責任範囲が違う。
Modelは内部データに近い。DTOはアプリケーションのデータ境界に近い。OpenAPIはHTTP通信の契約に近い。
一つのプロジェクトで複数の方式を組み合わせてもいい。たとえば、内部の管理画面ではModel由来の型を使い、公開APIではDTOとOpenAPIを使う。すべてを一つのルールに統一するより、データの流れに合わせて起点を選ぶほうが破綻しにくい。
Laravel公式エコシステム「Wayfinder」が扱う範囲
Laravelの型連携を考えるとき、公式エコシステムに含まれるWayfinderも候補になる。
Wayfinderは、Laravelアプリケーションを静的解析し、ルート、バリデーション規則、Model、EnumなどからTypeScriptの型や関数を自動生成する仕組みだ。SurveyorとRangerのパッケージを含む構成で、データ型だけではなく、Laravelアプリケーションの振る舞いに近い部分まで対象にする。
ここが、Modelだけを抽出するライブラリや、APIスキーマを生成するツールとの違いになる。
Laravelのルートをフロントエンドから呼び出すとき、URLの文字列を直接書く運用は小規模なら問題になりにくい。しかし、ルートパラメータが増え、名前付きルートが増え、画面側からの呼び出し箇所が増えると、URLの変更が広範囲に影響する。
ルート情報やバリデーション規則をTypeScript側へ反映できれば、単なるデータ型の同期から、Laravelのアプリケーション構造そのものをフロントエンドへ伝える方向へ進められる。
Wayfinderを検討するタイミング
Wayfinderは、次のようなプロジェクトで検討しやすい。
- Laravelを中心としたモノリス構成
- LaravelのルートをTypeScript側から利用する
- Inertia.jsなど、サーバー側とフロントエンドの距離が近い
- ルートやバリデーションの変更漏れを減らしたい
- Laravel公式の方向性に寄せて、長期運用したい
逆に、Laravelが完全にAPIサーバーで、フロントエンドが別のアプリケーションとして独立している場合、WayfinderだけでAPI契約全体を担えるとは限らない。その場合はOpenAPIが中心になり、WayfinderはLaravel側のルート連携など、適用できる範囲で補助的に使う判断になる。
ここは、ツールの機能一覧だけでは判断しにくいところだ。静的解析で何を生成できるかだけでなく、生成物をどのアプリケーションが読み、どのタイミングで更新するのかを見る必要がある。
「公式だからすべての構成に最適」という考え方も危険だ。逆に、既存のライブラリだから避ける必要もない。プロダクトの境界と、現在の運用コストで決めるべきだ。
プロジェクト構成別に選ぶ、現実的な型連携
ここまでの内容を、プロジェクト構成ごとの判断に落とし込む。
LaravelとTypeScriptが同じアプリケーションにある場合
LaravelとTypeScriptを同じリポジトリで管理し、画面もLaravelのルートと密接に結びついているなら、ModelやDTO、Wayfinderを候補にする。
ただし、最初から全データを自動生成する必要はない。
まずは変更頻度が高いデータから始める。ユーザー情報、認証状態、注文や課金の状態、画面の表示条件など、TypeScript側で条件分岐が多い領域だ。
たとえば、Enumの値がLaravelとTypeScriptでずれている場合、見た目には単なるラベルの問題でも、実際には状態遷移の誤りにつながる。こうした箇所は、自動生成の投資対効果が高い。
Modelの構造をそのまま使えるなら、fumeapp/modeltyperや7nohe/laravel-typegenのようなModel起点の手法が候補になる。画面に公開するデータを明確に分けたいなら、SpatieのDTO連携へ進む。
Laravelのルートやバリデーション規則までTypeScriptに伝えたいなら、Wayfinderの適用範囲を確認する。
Laravel APIとVue.js・Reactを分離している場合
フロントエンドが別リポジトリで、LaravelをAPIサーバーとして利用しているなら、OpenAPIを中心に考えたほうが境界が明確になる。
この構成では、Modelの型を共有しても、クライアントが必要とするのはHTTP APIの契約だ。リクエスト、レスポンス、ステータスコード、エラー形式。フロントエンドは、LaravelのModelそのものを知らなくてもAPIを利用できる必要がある。
ScrambleなどでOpenAPIドキュメントを生成し、openapi-typescriptでTypeScript型へ変換する流れは、その責任分担に合っている。
ここでの運用ポイントは、生成物をGit管理するかどうかだ。
- CIやビルド時に毎回生成する
- 生成された型定義をリポジトリへコミットする
- APIスキーマだけを共有パッケージとして配布する
- フロントエンド側で特定のバージョンを参照する
どれを選ぶかは、チーム構成とデプロイフローによる。個人開発であれば、まずは生成された型定義をコミットし、差分をレビューできる状態にするだけでも十分な検証になる。
自動化の理想を先に追いかけて、更新の責任者が曖昧になるのが一番まずい。
小規模な個人開発で、まだAPI設計が固まっていない場合
個人開発では、仕様が頻繁に変わる。昨日作ったエンドポイントを、今日になって別の形に変えることもある。
この段階でOpenAPIを厳密に整備し始めると、実装より仕様書の更新に時間を使うことがある。もちろん将来の公開APIを見据えるなら早期導入も選択肢だが、運用できない仕組みは負債になる。
まずはModelやEnumの型生成から始める。あるいは、重要な画面だけDTOを導入する。変更頻度が高く、バグの影響が大きいデータに絞る。
個人開発の型連携で大事なのは、全体を完璧にすることではない。ユーザーの痛みにつながる不具合を、どこで早く見つけるかだ。
管理者しか使わない設定画面より、決済状態や公開状態に関わるフロントエンドのほうが優先順位は高い。型生成の対象を、コード量ではなくリスクで決める。
導入前に決めるべき「正」の置き場所
ツール選定より先に、データの正を決める必要がある。
ここでいう正とは、TypeScriptの型定義を生成するときに参照する唯一の基準だ。
Modelを正にするのか。DTOを正にするのか。OpenAPIを正にするのか。これを曖昧にしたまま、複数の生成ツールを使うと、同じ名前の型が複数の場所から生成される。
たとえば、Userという名前の型がModel由来とOpenAPI由来の両方に存在する。片方にはavatar_urlがあり、もう片方にはない。どちらを使うべきか、コードを見ただけでは分からない。
この状態は、手書きの型定義より悪化している可能性がある。自動生成されているため、開発者は正しいと思い込みやすいからだ。
導入前に、少なくとも次の方針を決めておきたい。
- 内部データの型はModelから生成する
- 外部公開データの型はDTOから生成する
- HTTP APIの契約はOpenAPIから生成する
- ルートやフォーム送信の連携はWayfinderの対象にする
- 同じ用途の型を複数の起点から生成しない
- 生成ファイルを直接編集しない
- 生成コマンドを開発者の手元だけに置かない
最後の項目は特に重要だ。
型生成が個人のローカル環境でしか実行されないと、更新漏れが起きる。Laravel側の変更をコミットしたのに、TypeScript側の生成物を更新していない。あるいは、生成ツールのバージョン差によって出力が変わる。
生成コマンドをプロジェクトのスクリプトとして定義し、CIやビルドの中で実行できるようにする。これだけで、仕組みが属人化しにくくなる。
自動生成しても、実行時のデータは検証する
LaravelとTypeScriptの型連携を進めると、TypeScriptの型があれば安全だと思いやすい。しかし、TypeScriptの型は基本的にコンパイル時の情報だ。実行時に外部から届くJSONが、型定義どおりであることを保証するものではない。
APIサーバーのバージョンがずれる。キャッシュされた古いフロントエンドが残る。外部サービスのレスポンスが変わる。Laravel側の例外処理が想定外の形式で返る。
こうしたケースでは、生成された型定義だけでは防げない。
ここで必要になるのが、型連携の目的を分けて考えることだ。
- 開発中のプロパティ名や構造のミスを見つける
- API仕様の変更を差分として把握する
- 実行時に受け取ったデータの妥当性を確認する
- ユーザーへ表示するエラーを安全に処理する
自動生成ツールが主に助けてくれるのは、最初の2つだ。実行時の検証まで必要な領域では、別のバリデーションやパーサーの仕組みを検討する必要がある。
ここを混同して、「型が生成されているからAPIは安全」と断定するのは危険だ。型定義の自動生成は、型安全な開発の一部であって、すべてではない。
私ならこう段階導入する
新しい個人開発プロジェクトであれば、私は次の順番で導入する。
最初はEnumと頻出Modelから始める
最初から全Modelを対象にしない。フロントエンドで利用頻度が高く、値のズレが不具合につながりやすいEnumや主要Modelから始める。
この段階の目的は、自動生成の仕組みを理解することではない。生成、確認、コミット、ビルドという運用ループを作ることだ。
次にDTOを導入して公開データを分離する
APIのレスポンスが複雑になってきたら、Model直結からDTOへ移行する。
一覧、詳細、編集、認証ユーザーなど、用途の違うデータを一つの型にまとめない。DTOの数が増えることを恐れず、データの意味を分ける。
ただし、すべての処理を一度に書き換えない。新しく作るエンドポイントからDTOを使い、既存APIは変更のタイミングで段階的に移行する。
APIを外部契約として扱う段階でOpenAPIへ進む
フロントエンドが別リポジトリになる。モバイルアプリが増える。外部連携を始める。この段階では、Modelの型を共有するだけでは足りない。
Scrambleなどを使ったOpenAPI生成と、openapi-typescriptによるTypeScript型生成を導入する。ここで初めて、APIの契約をフロントエンドから独立した仕様として扱える。
Laravel中心の構成ではWayfinderを検証する
Laravelのルート、バリデーション、Model、Enumを横断してTypeScriptへ伝えたいなら、Wayfinderを検証する。
ただし、プロジェクト全体の構造を大きく変える前に、実際の画面で生成物が使いやすいかを見る。生成された関数や型が、現在のルーティングやフォーム処理にどれだけ自然に接続できるか。ここはドキュメントだけではなく、手元のコードで確認する部分だ。
この順番なら、導入効果を小さく検証しながら、必要な境界だけを強化できる。
ツール比較ではなく、変更の流れで評価する
LaravelとTypeScriptの型定義自動生成を比較するとき、機能一覧だけを見ると迷う。
Model対応か。DTO対応か。Enum対応か。ルート対応か。OpenAPI対応か。もちろん確認は必要だが、実際に重要なのは、変更が起きたときの流れだ。
たとえば、Laravel側で属性名を変更したとする。
1. どのファイルを変更するのか
2. どのコマンドを実行するのか
3. 生成物の差分をどこで確認するのか
4. TypeScript側のどのコードが壊れるのか
5. CIで検出できるのか
6. API利用者へ変更をどう伝えるのか
この一連の流れが短く、明確であるほど、そのツールはプロジェクトに合っている。
逆に、生成コマンドはあるが、生成物を誰も確認していない。型は更新されるが、実際のAPIレスポンスは別の処理で作られている。こうなると、自動生成は安心感だけを提供する仕組みになってしまう。
個人開発では、ツールの導入コストも重要だ。設定ファイルが増え、依存パッケージが増え、更新時に毎回調査が必要になるなら、型生成によるメリットと釣り合わない可能性がある。
だからこそ、指標を置く。
- 手書き型定義の重複がどれだけ減ったか
- API変更時の修正漏れが減ったか
- 型生成にかかる時間が許容範囲か
- 生成物の差分をレビューできているか
- フロントエンドのビルドで破壊的変更を検出できるか
- 開発者が生成元と生成先を説明できるか
成功の基準は、導入したツールの数ではない。ユーザーの痛みに直結する不具合を、リリース前に見つけられるようになったかだ。
LaravelとTypeScriptの型共有で避けたい失敗
Modelの全属性をそのまま公開する
最も分かりやすい失敗だ。Modelにあるから、TypeScriptにも出す。これでは内部情報と公開情報の境界がなくなる。
Model起点の型は便利だが、APIの公開契約として使う場合は、ResourceやDTOとの整合性を確認する必要がある。
生成ファイルを手で修正する
生成結果に足りない型があると、直接ファイルを編集したくなる。しかし、次の生成で消える。
一時的な検証なら構わないが、継続利用するなら生成元を修正する。DTOを追加する。変換ルールを調整する。OpenAPIの仕様を見直す。手間はかかるが、そこを飛ばすと自動化の意味が薄れる。
型名を用途で分けない
User、Product、Orderのような名前だけでは、データの利用場面が伝わらない。
一覧用、詳細用、入力用、レスポンス用。必要に応じて意味を分ける。型の数を減らすことより、誤った使い回しを減らすことを優先したい。
APIの変更を型生成だけで終わらせる
型が更新されても、画面の挙動が正しいとは限らない。nullの表示、空配列、ページネーション、エラー時のレスポンスなど、実際のUIと通信処理まで確認する。
型は変更の入口を教えてくれる。最終的なユーザー体験を保証するものではない。
すべての境界を一つのツールで解決しようとする
Model、DTO、API、ルート、バリデーションは、それぞれ別の責任を持つ。単一のツールで全部を解決できると考えるより、どの境界をどの仕組みで管理するかを決めたほうがいい。
現時点で、Model・DTO・REST APIレスポンス・バリデーションのすべてを、追加設定なしで完璧に同期する万能なライブラリがあると断定することはできない。ツールごとに適用対象が異なるからだ。
結論:最適解は「どこまで自動化しないか」まで決めること
LaravelとTypeScriptの型定義自動生成は、導入すれば終わる機能ではない。
Eloquent Modelから直接型を作るなら、導入は軽く、既存コードにも入りやすい。一方で、ModelとAPIレスポンスの差を吸収する必要がある。Spatieのlaravel-typescript-transformerを使ってDTOから生成するなら、公開データの契約を明確にできる。その代わり、DTOを実際の処理の中心に置く運用が必要になる。
分離されたAPIとフロントエンドを連携するなら、ScrambleとOpenAPI、openapi-typescriptの組み合わせが自然だ。HTTP通信の契約を中心に据えられる。Laravelを中心としたモノリス構成で、ルートやバリデーションまでTypeScriptと共有したいなら、Wayfinderの可能性を検証したい。
選定の基準は、機能数ではない。
どのデータがユーザーに公開されるのか。どこで仕様が決まるのか。変更をどのタイミングで検知したいのか。生成物を誰が管理するのか。そこを言語化することだ。
私が次に試したいのは、APIスキーマの差分をCIで検出し、TypeScript側のビルドまで一つの変更フローとしてつなぐことだ。型が生成されるだけでなく、契約変更がどの画面に影響するのかを、プルリクエストの段階で見えるようにする。
自動生成の価値は、書かなくて済むコードの量では測れない。仕様の変更が、ユーザーに届く前に開発者へ届く。その距離を短くできるかどうか。LaravelとTypeScriptを組み合わせるなら、まずそこから検証したい。
Related reading: LaravelのAPI連携におけるRESTとGraphQLの長所と個人開発での最適解.