vagebond/runtype
Runtype brings lightweight runtime type checking to PHP and Laravel. Define expected types and validate values as your app runs, catching mismatches early and making data flow safer and clearer without relying solely on static analysis.
Install it as a dev dependency so it never ships to production:
composer require vagebond/runtype --dev
Publish the config (this is the first file you should read once it exists):
php artisan vendor:publish --tag="runtype-config"
Then run the generator once to see what it produces before writing any frontend code against it:
php artisan runtype:generate
Open three things in this order:
config/runtype.php — paths it scans, output directory, naming conventions, and any type-mapping overrides.resources/js/types/) — confirm the shape matches your Resources, not just your models.app/Http/Resources/*.php file next to the generated interface, so you can see the mapping in both directions.First use case: pick a single Resource you already expose over JSON to your Vue/React/Inertia frontend (e.g. UserResource). Generate, then replace your hand-written interface User with the generated one. If the interface matches what your frontend actually receives, you've validated the whole pipeline; if it doesn't, you've found the config knob you need before scaling to the rest of the app.
This package is a dev-time codegen tool, not a runtime bridge. Treat it like ziggy:generate or wayfinder — it belongs in your local workflow and CI, not in deploy hooks.
Resource-first, model-second. Resources are the contract your frontend actually sees; models leak internal columns. Point the generator at app/Http/Resources and use model scanning only for shared/embedded shapes (e.g. CountryResource embedded inside AddressResource). If a model has hidden attributes ($hidden, $appends), don't let it drift into your output by generating from models directly.
Wire generation into your dev loop. Two reliable options:
composer types works everywhere: "types": ["@php artisan runtype:generate"].package.json dev pipeline: "dev": "php artisan runtype:generate && vite". This guarantees types are fresh when Vite starts, which catches drift the moment you switch branches.Prefer the Composer script for CI (composer types --no-interaction then git diff --exit-code resources/js/types) so stale generated types fail the build rather than silently rot.
Group by feature, not by "Resources" folder. If your Resources map to domains (Billing, Auth, Catalog), nest the generated output the same way and add one barrel index.ts per domain. It keeps frontend imports stable when you split or rename a Resource later.
Pair with runtime validation at the boundary. Generated interfaces are compile-time only. For API responses that cross trust boundaries (or evolve without versioning), add Zod/Yup schemas derived from the same Resources, or use a single parseUser() function per Resource that wraps the fetch. Otherwise TypeScript will cheerfully type-check undefined into your components.
Use it as a review artifact. When a PR changes a Resource, the regenerated diff should appear in the PR. Reviewers get a precise, typed summary of the API contract change — that's arguably more valuable than the codegen itself. To enable this, commit the generated directory and don't gitignore it.
Integrate with Laravel's Resource lifecycle. Because Resource::toArray can branch on when()/mergeWhen(), generated types are the union of possible shapes. For endpoints where the shape is conditional, either stabilize the Resource (extract variant Resources like UserSummaryResource vs UserDetailResource) or wrap the generated type in your frontend with a discriminated union keyed on a field you control.
when() and mergeWhen() are invisible to static analysis. The generator cannot know a runtime condition's result, so field-presence is either best-effort or you get an "always present" type that lies at runtime. Rule of thumb: if a field appears inside when(), treat the generated type as T | undefined in your consumer, or split the Resource. This is the single biggest source of "types compile, app crashes" bugs with any Resource-based generator.
Accessors and appended attributes. Model accessors (getFullNameAttribute) and $appends entries don't map cleanly to database columns. Verify they appear in the generated interface; if the package only reads $fillable/casts, you'll need to declare them explicitly in the Resource ('full_name' => $this->full_name) — which is often a good refactor anyway.
Dates are strings, decimals are strings. created_at becomes string, and depending on your database driver, money/decimal columns may serialize as "19.99" (string) rather than number. Check one generated interface against a real JSON response early; adjust casts in the Resource if the mismatch matters to your UI.
Enums need a decision. Backed enums serialize to their scalar (string/int) by default. If you want literal-union types or a TS enum, either configure the mapping or manually define enums and reference them from your Resource — don't assume the package infers a TS enum from a PHP enum.
Stale output is the silent failure mode. Nothing breaks when types drift; the app just gets subtly wrong autocomplete and passes the type-checker while failing at runtime. Mitigations: (1) generate as a pre-vite step, (2) add a CI check that regenerates and diffs, (3) commit the output. Pick at least two.
Naming collisions are real. Two Resources with the same short name in different namespaces produce two files, and whichever is imported last wins in a barrel export. Prefix generated filenames with the domain (Billing/Invoice.ts, Catalog/Invoice.ts) or configure the generator's naming template before you have fifty files to rename.
Output paths are not always safe to overwrite. If your configured output directory contains hand-written files (a shared api.d.ts, custom helper types), don't let the generator own the whole directory. Generate into types/generated/ and keep hand-written types adjacent in types/. This makes "delete generated, regenerate" a safe recovery step.
Config quirks to check first when something's wrong: the scan paths (is the path relative to base_path or absolute?), the "exclude" patterns (a .gitignore-style glob vs. regex — behavior differs), and whether generation is memoized/cached (rare, but if a new Resource isn't picked up, clear the cache before debugging deeper).
Escape hatch for one-off types. You'll occasionally have a Resource whose shape is too dynamic to express (polymorphic morphs, $this->when($permission, ...) matrices). Don't fight it. Generate the 95% that's static, and hand-maintain a small overrides/ directory imported via path mapping. That's a healthy use of the tool, not a failure of it.
Version pinning. This is a 26-star package with zero dependents and a future-dated release. Pin an exact version in composer.json ("vagebond/runtype": "x.y.z") and read the diff when you bump — there is no ecosystem pressure forcing backward compatibility, and the underlying Laravel Resource API (whenAggregated, whenCounted) is itself still evolving. Budget a half-day every time you upgrade Laravel.
How can I help you explore Laravel packages today?