🔧

型定義書くの面倒?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::renderusePageLinkコンポーネント等)を理解している

まだ環境が整っていない場合は、以下のコマンドでプロジェクトを作成することができます。

laravel new my-app --react
cd my-app

Laravel Data + TypeScript Transformerによる型同期の仕組み

この手法では、2つのパッケージが連携して型安全性を実現します。

  • Laravel Data: PHPで 明示的なデータ構造(DTO) を定義
  • TypeScript Transformer: PHP構造からTypeScript型定義を自動生成

基本的な流れ

  1. Laravel Data クラスでデータ構造を定義(PHP側)
  2. TypeScript TransformerがPHP構造を解析してTypeScript型定義を自動生成
  3. Inertia.jsでデータをフロントエンドに渡す際にData クラスを使用
  4. 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を使用しない場合でも同じですね。

app/Data/UserData.php
<?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があればそちらも自動的に生成されます。

resources/types/generated.d.ts
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の設定を更新します。

tsconfig.json
{
    "compilerOptions": {
        // ... その他の設定
    },
    "include": [
        "resources/js/**/*",
        "resources/types/generated.d.ts"  // ← この行を追加
    ]
}

これで App.Data.UserData 型が正しく認識されるようになります。

5. Controllerでの使用

app/Http/Controllers/UserController.php
<?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での使用

resources/js/Pages/Users/Show.tsx
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 設定で最大変換深度を制限することで対処できます。

config/data.php
'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 Transformerphp artisan typescript:transform を実行するだけで、PHPからTypeScript型定義を自動生成できます。これで、フロントエンドで型定義を手動で書く必要がなくなり、any 型に頼ることなく型安全性を保てるようになります。

この方法で 「型定義を書く手間」「型安全性」 を両立できるので、開発速度と品質を更に向上させることができるはずです!

ここに書ききれなかった便利な機能もあるので、ぜひ公式ドキュメントも確認してみてください。

https://spatie.be/docs/laravel-data

https://spatie.be/docs/typescript-transformer

AUN Tech Blog

Discussion