Monolit modern yang memadukan Laravel dan Inertia.js sering mengalami masalah silent drift: backend mengubah field DTO atau model, namun frontend TypeScript tetap menggunakan definisi tipe usang tanpa memicu compile error. Masalah ini baru terlihat saat aplikasi berjalan di production dan antarmuka menerima payload yang tidak sesuai.

Solusi yang teruji adalah memperlakukan backend PHP sebagai single source of truth, menghasilkan definisi tipe TypeScript secara otomatis (typegen), mengaitkannya ke reactive watcher pada Vite, serta memvalidasi sinkronisasi file hasil generasi pada CI pipeline.

1. Konfigurasi Backend: spatie/laravel-typescript-transformer

Instal paket transformer resmi untuk Laravel:

composer require spatie/laravel-typescript-transformer

Publikasikan konfigurasi melalui artisan:

php artisan vendor:publish --tag=typescript-transformer-config

Edit config/typescript-transformer.php. Tentukan direktori sumber DTO dan jalur output definisi TypeScript frontend:

return [
    'searching_paths' => [
        app_path('Data'),
        app_path('DTO'),
    ],
    'transformers' => [
        Spatie\TypeScriptTransformer\Transformers\DtoTransformer::class,
    ],
    'output_file' => resource_path('js/types/generated.d.ts'),
    'writer' => Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter::class,
];

Definisi DTO PHP

Gunakan atribut #[TypeScript] pada class representasi data untuk mengekspor definisi skema:

<?php

namespace App\Data;

use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
class UserData
{
    public function __construct(
        public int $id,
        public string $name,
        public string $email,
        public ?string $avatarUrl,
        /** @var array<string> */
        public array $permissions,
    ) {}
}

Jalankan sinkronisasi manual pertama:

php artisan typescript:transform

Hasil transformasi pada resources/js/types/generated.d.ts:

declare namespace App.Data {
    export type UserData = {
        id: number;
        name: string;
        email: string;
        avatarUrl: string | null;
        permissions: Array<string>;
    };
}

2. Otomasi DX: Vite Hot Watcher

Menjalankan perintah Artisan secara manual setiap kali memperbarui PHP DTO memperlambat alur kerja. Gunakan hook Vite bawaan untuk memantau perubahan file PHP di direktori target tanpa menambah dependensi watcher eksternal.

Tambahkan custom plugin minimal pada vite.config.ts:

import { defineConfig, Plugin } from 'vite';
import laravel from 'laravel-vite-plugin';
import { exec } from 'node:child_process';

function typegenWatcher(): Plugin {
    return {
        name: 'inertia-typegen-watcher',
        handleHotUpdate({ file, server }) {
            if (file.includes('/app/Data/') && file.endsWith('.php')) {
                exec('php artisan typescript:transform', (error) => {
                    if (error) {
                        console.error('Typegen error:', error);
                        return;
                    }
                    server.ws.send({ type: 'full-reload' });
                });
            }
        },
    };
}

export default defineConfig({
    plugins: [
        laravel({
            input: 'resources/js/app.ts',
            refresh: true,
        }),
        typegenWatcher(),
    ],
});

3. Pre-commit Verification: Husky & lint-staged

Cegah developer commit kode PHP tanpa meregenerasi file TypeScript. Gunakan lint-staged untuk mengeksekusi typegen jika ada perubahan pada file DTO yang di-stage.

Tambahkan konfigurasi di package.json:

{
  "scripts": {
    "typegen": "php artisan typescript:transform"
  },
  "lint-staged": {
    "app/Data/**/*.php": [
      "php artisan typescript:transform",
      "git add resources/js/types/generated.d.ts"
    ]
  }
}

Daftarkan hook pre-commit pada .husky/pre-commit:

npx lint-staged

4. Deteksi Drift di CI: GitHub Actions Pipeline

Pre-commit hook di lokal dapat dilewati menggunakan flag --no-verify. Pipeline CI menjadi gerbang validasi akhir yang bersifat mutlak.

Prinsip kerjanya: CI menjalankan php artisan typescript:transform, lalu mengecek apakah git working tree mendeteksi perubahan menggunakan git diff --exit-code. Jika ada diff, berarti pengembang mengubah backend tanpa menyertakan regenerasi file generated.d.ts terbaru.

Implementasi workflow pada .github/workflows/ci.yml:

name: CI Checks

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  type-safety:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, json
          coverage: none

      - name: Install Composer dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install NPM dependencies
        run: npm ci

      - name: Verify Type Definition Parity
        run: |
          php artisan typescript:transform
          git diff --exit-code resources/js/types/generated.d.ts

      - name: Run TypeScript typecheck
        run: npx vue-tsc --noEmit

5. Konsumsi Type pada Komponen Inertia

Gunakan type yang dihasilkan pada komponen frontend dengan memetakan props halaman secara eksplisit:

<script setup lang="ts">
interface Props {
    user: App.Data.UserData;
}

const props = defineProps<Props>();
</script>

<template>
    <div class="profile">
        <h1>{{ props.user.name }}</h1>
        <p>{{ props.user.email }}</p>
    </div>
</template>

Catatan Teknis dan Trade-offs

  • Model Eloquent vs DTO: Hindari mengekspor Eloquent Model secara langsung menggunakan transformer. Model memiliki mutator, accessor, dan serialization logic dinamis yang sulit diurai secara statis. Gunakan DTO flat atau Spatie Laravel-Data sebagai kontrak data.
  • Overhead Performa Watcher: Eksekusi php artisan via Node exec menimbulkan overhead proses spawning baru (~100-300ms). Jangan pasang watcher pada path luas seperti app/**/*.php, batasi spesifik pada namespace app/Data.
  • CI Failures: Pastikan composer autoloader diperbarui sebelum step typescript:transform dijalankan di CI untuk menghindari class not found saat scanning file PHP baru.