Skip to content

Type Transformation Pipeline

Vroum Solutions maintains strong typing between backend PHP domain objects and frontend TypeScript through automated transformation.

Source of Truth

  • PHP Data Transfer Objects (DTOs) in app/Data/ using spatie/laravel-data.
  • Backed Enums in app/Entities/ using foxbytehq/laravel-backed-enums.

Transformation Tooling

  • spatie/laravel-typescript-transformer scans the classes selected in config/typescript-transformer.php.
  • Project-specific transformers live in app/Transformers (DataTypeScriptTransformer, EnumTransformer, TransformedTypeEnum) and app/Writers/ModuleWriter.
  • Output:
    • resources/js/types/generated.ts — interfaces for Data objects and enum types
    • resources/js/types/enumRegistry.ts — runtime registry produced by the generate:enum-registry artisan command, carrying each enum's metadata (label, color, icon)

Workflow

  1. Developer creates/updates a Data class or Enum.
  2. The Vite watch plugin sees the change under app/{Data,Entities}/**/*.php and runs composer run transform-types.
  3. That script runs artisan typescript:transform --format, then artisan generate:enum-registry.
  4. Frontend imports from @/types/generated.ts and @/types/enumRegistry.ts.

Manual run:

bash
composer run transform-types

Example Data Class

Only classes marked #[TypeScript] are exported.

php
declare(strict_types=1);

namespace App\Data;

use App\Entities\OvertimeEligibility;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

#[TypeScript]
class UserWorkspaceData extends Data
{
    public function __construct(
        public ?int $preferred_bay_id,
        /** @var string[] */
        public array $brand_specialties,
        public int $max_concurrent_work_orders,
        public OvertimeEligibility $overtime_eligibility,
    ) {}
}

Generated TS (conceptual):

ts
export interface UserWorkspaceData {
    preferred_bay_id: number | null;
    brand_specialties: string[];
    max_concurrent_work_orders: number;
    overtime_eligibility: OvertimeEligibility;
}

Note the array @var docblock — the transformer needs it to emit string[] instead of unknown[].

Example Enum

php
declare(strict_types=1);

namespace App\Entities;

use App\Traits\IsBackedEnum;
use Foxbytehq\LaravelBackedEnums\BackedEnum;

enum TirePosition: string implements BackedEnum
{
    use IsBackedEnum;

    case FRONT_LEFT = 'front_left';
    case FRONT_RIGHT = 'front_right';
    case REAR_LEFT = 'rear_left';
    case REAR_RIGHT = 'rear_right';
}

Enum Wire Format & Hydration

Enums do not cross the wire as bare strings. They are serialized as an envelope:

json
{ "__enum": "TirePosition", "value": "front_left" }

hydrateEnums() (in @/Utils/HydrateEnums.ts) is called from the Inertia resolve hook in resources/js/app.ts, so it runs before every page render — initial load, visits, partial reloads and history restores alike. It walks the props in place and swaps every envelope for the matching EnumValue instance from enumRegistry.ts (value plus label/color/icon metadata). History-restored props are plain clones that still carry __enum, so they get rebuilt the same way.

Consequences:

  • Never compare a prop against the raw envelope object; use the hydrated EnumValue.
  • An [hydrateEnums] Unknown enum "…" console warning means the registry is stale — run composer run transform-types.
  • Anything that bypasses the Inertia page pipeline (a raw useHttp JSON response, a broadcast payload) is not hydrated automatically — hydrate it yourself if you need the metadata.

Guidelines

  • Keep Data constructors simple, with explicit scalar & object types.
  • Avoid exposing model instances directly; wrap them in Data objects.
  • Keep enums small & expressive; display metadata belongs in the enum's IsBackedEnum methods, not scattered in components.

Safety & Consistency

  • DO NOT manually edit generated.ts or enumRegistry.ts; changes are overwritten.
  • TS consumers should rely on these definitions for props, store state, and API payload typing.

See standards.md for naming & style rules; proceed to troubleshooting.md if types aren't regenerating.