型定義書くの面倒?Laravel Data + TypeScript Transformerがあるよ!
要約
-
お悩み:Laravel + Inertia.jsで型を手動で書くのが面倒で、結局
any型を使ってしまう - 解決方法:Laravel Data + TypeScript TransformerでPHPからTypeScript型を自動作成
-
やり方:
#[TypeScript]を付けてphp artisan typescript:transformを実行するだけ - 効果:タイプミスをすぐ発見、IDE補完が効く、エラーが減り、開発速度が向上
結論: 型定義を手動で書く必要がなくなり、any型を使わずに済むので、Laravel + Inertia.js開発がもっと楽になる
はじめに
最近、チームのエンジニアが Laravel Starter Kits(Laravel + Inertia.js + React/Vue)を使い始めて、「爆速で開発できるようになった!」 と気に入ってくれているようでした!
しかし雑談しているときに、「型定義をフロントエンドに手動で書く必要があって、個人開発やプロトタイプ版では結局 any 型を使うようになってしまう」という話を聞いてしました。それはいけない。
// Laravel Controller
return Inertia::render('UserProfile', [
'user' => $user, // User モデルのどのプロパティが渡されるか不明
]);
// React Component
interface Props {
user: any; // any型に頼ってしまう...
}
確かに、Laravel と Inertia.js を組み合わせた開発では、バックエンドのデータ構造をフロントエンドで正しく型付けする作業が手間になりがちです。
そこで 「PHPからTypeScriptに型を自動同期する方法がある」 と伝えると、興味を示してくれたので、Laravel Data + TypeScript Transformer を使って、バックエンドからフロントエンドまで一貫した型安全性を実現する方法を記事にしてみました。
前提条件
この記事では、以下の環境が既に構築されていることを前提とします。
- Laravel 11/12 + Inertia.js + React + TypeScript の環境
- Laravel Starter Kits を使用している場合も含む
- 基本的なInertia.jsの概念(
Inertia::render、usePage、Linkコンポーネント等)を理解している
まだ環境が整っていない場合は、以下のコマンドでプロジェクトを作成することができます。
laravel new my-app --react
cd my-app
Laravel Data + TypeScript Transformerによる型同期の仕組み
この手法では、2つのパッケージが連携して型安全性を実現します。
- Laravel Data: PHPで 明示的なデータ構造(DTO) を定義
- TypeScript Transformer: PHP構造からTypeScript型定義を自動生成
基本的な流れ
- Laravel Data クラスでデータ構造を定義(PHP側)
- TypeScript TransformerがPHP構造を解析してTypeScript型定義を自動生成
- Inertia.jsでデータをフロントエンドに渡す際にData クラスを使用
- React/Vue Componentで生成された型定義を使用
各パッケージの役割
| パッケージ | 役割 | 具体的な機能 |
|---|---|---|
| Laravel Data | データ構造定義 | DTO作成、バリデーション、データ変換 |
| TypeScript Transformer | 型同期 | PHP→TypeScript型変換、#[TypeScript]属性の処理 |
実装手順
1. 必要なパッケージのインストール
Laravel DataとTypeScript Transformerパッケージを追加します。
# Laravel Data(データ構造定義用)
composer require spatie/laravel-data
# TypeScript Transformer(型同期用)
composer require spatie/typescript-transformer
# TypeScript変換コマンド実行に必須
composer require spatie/enum
# Laravel Data の設定ファイル(オプション)
php artisan vendor:publish --provider="Spatie\LaravelData\LaravelDataServiceProvider" --tag="data-config"
# TypeScript Transformer の設定ファイル(オプション)
php artisan vendor:publish --tag="typescript-transformer-config"
2. Data クラスの作成
Artisanコマンドを使用してData クラスを生成します。
# UserData クラスを生成
php artisan make:data UserData
生成されたファイルを編集します。Laravel Dataは、ネストしたデータ構造(他のDataクラスを含む)や配列型も自動的に処理します。
また、セキュリティ配慮として、フロントエンドに露出すべきでないプロパティは含めないようにします。これについてはLaravel Dataを使用しない場合でも同じですね。
<?php
namespace App\Data;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
class UserData extends Data
{
public function __construct(
public int $id,
public string $name,
public string $email,
public ?string $email_verified_at,
public string $created_at,
public string $updated_at,
// ❌ 以下のプロパティは含めない(セキュリティ上の理由)
// public string $password,
// public string $remember_token,
// public ?string $two_factor_secret,
// public ?string $two_factor_recovery_codes,
// ネストしたデータ構造の例(オプション)
/** @var PostData[] */
#[DataCollectionOf(PostData::class)]
public array $posts,
) {}
}
3. TypeScript型の生成
TypeScript TransformerがPHP構造を解析してTypeScript型定義を生成します。
php artisan typescript:transform
生成されるTypeScript型定義は以下のようになります。
詳しい説明は省略しますが、Enumがあればそちらも自動的に生成されます。
declare namespace App.Data {
export type UserData = {
id: number;
name: string;
email: string;
email_verified_at: string | null;
created_at: string;
updated_at: string;
posts: Array<App.Data.PostData>; // ネストしたデータ構造
};
export type PostData = {
id: number;
title: string;
status: App.Enums.PostStatus;
// ... 他のプロパティ
};
}
// Enumの例
declare namespace App.Enums {
export type PostStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
}
4. TypeScript設定の更新
生成された型定義ファイルをTypeScriptが認識できるように、tsconfig.jsonの設定を更新します。
{
"compilerOptions": {
// ... その他の設定
},
"include": [
"resources/js/**/*",
"resources/types/generated.d.ts" // ← この行を追加
]
}
これで App.Data.UserData 型が正しく認識されるようになります。
5. Controllerでの使用
<?php
namespace App\Http\Controllers;
use App\Data\UserData;
use App\Models\User;
use Inertia\Inertia;
class UserController extends Controller
{
public function show(User $user)
{
return Inertia::render('Users/Show', [
'user' => UserData::from($user),
]);
}
}
6. React Componentでの使用
import React from 'react';
// 型安全なProps定義
interface Props {
user: App.Data.UserData;
}
export default function UserShow({ user }: Props) {
return (
<div className="user-profile">
<h1>{user.name}</h1>
<div className="user-info">
<p>Email: {user.email}</p>
<p>Email Verified: {user.email_verified_at ? 'Yes' : 'No'}</p>
<p>Member since: {new Date(user.created_at).toLocaleDateString()}</p>
<p>Last updated: {new Date(user.updated_at).toLocaleDateString()}</p>
</div>
</div>
);
}
開発効率への影響
型同期を導入することで、このような効果があります。
| 項目 | Before(型同期なし) | After(型同期あり) |
|---|---|---|
| エラー検出 | ❌ ランタイムエラーが頻発console.log(user.name); // undefined エラー
|
✅ コンパイル時エラーで事前検出// 存在しないプロパティはコンパイルエラー
|
| IDE支援 | ❌ 補完が効かないuser. // 補完なし
|
✅ 自動補完でプロパティ一覧表示user. // id, name, email... が表示
|
| 変更の追跡 | ❌ バックエンドの変更に気づかない// プロパティ名変更時も無反応
|
✅ 自動的に変更を検出// バックエンド変更時にコンパイルエラー
|
| 開発速度 | ❌ デバッグに時間がかかる// ランタイムエラーの調査・修正
|
✅ 高速な開発サイクル// コンパイル時に問題を解決
|
| 型安全性 | ❌ any 型に依存interface Props { user: any; }
|
✅ 完全な型安全性interface Props { user: App.Data.UserData; }
|
Before: 型同期なしの場合
// ❌ 問題のあるコード例
function UserProfile({ user }: { user: any }) {
// プロパティ名のタイプミスに気づかない
return <h1>{user.naem}</h1>; // 'name' のつもりが 'naem'
// 存在しないプロパティにアクセス
console.log(user.profile.bio); // user.profile が undefined でエラー
}
After: 型同期ありの場合
// ✅ 型安全なコード例
function UserProfile({ user }: { user: App.Data.UserData }) {
// タイプミスは即座にコンパイルエラー
return <h1>{user.name}</h1>; // 正しいプロパティ名のみ使用可能
// 型定義に基づいた安全なアクセス
return (
<div>
<p>Email: {user.email}</p>
<p>Verified: {user.email_verified_at ? 'Yes' : 'No'}</p>
</div>
);
}
他の選択肢
Laravelの型定義をTypeScriptへ自動生成・同期するためのパッケージやアプローチは他にもいくつか選択肢があります。
| パッケージ名 | 概要 | 特徴 |
|---|---|---|
| Laravel Data + TypeScript Transformer | DTO→TS型 | 豊富な機能(キャスト、変換、TypeScript生成等)、同じSpatie製 |
| Laravel Validated DTO + TypeScript Transformer | DTO→TS型 | バリデーション再利用重視 |
| Laravel TypeScript | モデル→TS型 | Eloquentモデルを直接変換 |
| Laravel Typegen | モデル→TS型 | Eloquentモデルを直接変換。Enumも対応 |
設計思想がそれぞれ異なるため、プロジェクトの要件や既存構成に応じて選択するのが良いと思います。
注意点
循環参照の問題
ネストしたデータ構造を扱う際に、循環参照(例:UserがPostを持ち、PostがUserを参照する)が発生する可能性があります。Laravel Dataでは max_transformation_depth 設定で最大変換深度を制限することで対処できます。
'max_transformation_depth' => 20,
'throw_when_max_transformation_depth_reached' => true, // 例外をスロー
参考: Transformers | laravel-data | Spatie
複雑な型変換の制限
PHPとTypeScriptの型システムの違いにより、一部の複雑な型は自動変換に制限があります。
-
Union型: PHPの
string|int→ TypeScript のstring | numberに変換可能 -
日付型: 設定した形式(デフォルト:
DATE_ATOM)で文字列として出力 -
Optional/Lazy型: TypeScriptでは
property?: typeとして表現される
参考: TypeScript | laravel-data | Spatie
まとめ
今回は、チームメンバーの「型定義を手動で書くのが面倒で、結局 any 型を使ってしまう」という相談から、Laravel Data + TypeScript Transformer を使った型同期の方法を記事にしてみました。
Laravel Data でデータ構造を定義して、TypeScript Transformer で php artisan typescript:transform を実行するだけで、PHPからTypeScript型定義を自動生成できます。これで、フロントエンドで型定義を手動で書く必要がなくなり、any 型に頼ることなく型安全性を保てるようになります。
この方法で 「型定義を書く手間」 と 「型安全性」 を両立できるので、開発速度と品質を更に向上させることができるはずです!
ここに書ききれなかった便利な機能もあるので、ぜひ公式ドキュメントも確認してみてください。
修正指示をスムーズにする校正ツール「AUN(aun.tools)」を広島を拠点に開発・運営している、株式会社フォノグラムのテックブログです。 エンジニア熱烈❤️🔥募集中です!
Discussion