Appearance
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/usingspatie/laravel-data. - Backed Enums in
app/Entities/usingfoxbytehq/laravel-backed-enums.
Transformation Tooling
spatie/laravel-typescript-transformerscans the classes selected inconfig/typescript-transformer.php.- Project-specific transformers live in
app/Transformers(DataTypeScriptTransformer,EnumTransformer,TransformedTypeEnum) andapp/Writers/ModuleWriter. - Output:
resources/js/types/generated.ts— interfaces for Data objects and enum typesresources/js/types/enumRegistry.ts— runtime registry produced by thegenerate:enum-registryartisan command, carrying each enum's metadata (label, color, icon)
Workflow
- Developer creates/updates a Data class or Enum.
- The Vite watch plugin sees the change under
app/{Data,Entities}/**/*.phpand runscomposer run transform-types. - That script runs
artisan typescript:transform --format, thenartisan generate:enum-registry. - Frontend imports from
@/types/generated.tsand@/types/enumRegistry.ts.
Manual run:
bash
composer run transform-typesExample 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 — runcomposer run transform-types. - Anything that bypasses the Inertia page pipeline (a raw
useHttpJSON 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
IsBackedEnummethods, not scattered in components.
Safety & Consistency
- DO NOT manually edit
generated.tsorenumRegistry.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.