oi-lab/oi-laravel-ts
A Laravel package that automatically generates TypeScript interfaces from your Eloquent models, with relationship support, custom casts, DataObjects, PHPDoc types, watch mode, and namespace filters for customization.
Architecture fit. The package is a code-generation tool, not a runtime dependency. It reads Eloquent model metadata (casts, relationships, key types, PHPDoc) and emits a single interfaces.ts artifact consumed by the frontend. That puts it in the same architectural slot as generated API clients or GraphQL codegen: a build-time contract-enforcement layer between Laravel and TypeScript. For a TPM this is attractive because it is additive — no changes to model code, no service provider bootstrapping in the request path, no middleware. The two-pipeline design (Eloquent analysis → TypeScript emission) with an intermediate schema is a sound separation, and the save_schema toggle gives a debuggable inspection point when the output looks wrong. The data_replaces_model option is the strategically interesting one: it lets a DTO become the single source of truth for its model's wire shape, which is the correct model for APIs that return shaped payloads rather than raw rows.
Integration feasibility. Low friction. Composer install, publish one config file, run one artisan command. The documented surface is unusually complete for a package of this age: namespace filters, custom props, external type imports, UUID/ULID key detection, enum literal unions, nested DTOs, and a watch mode. The DTO support is structural rather than coupled to spatie/laravel-data, which removes a hard transitive dependency — a genuine plus. The Inertia.js example in the README is the obvious first target if the frontend is Inertia-based.
Technical risk. The dominant risk is not the code, it is the supply chain. The package has 1 star, 0 dependents, and a single maintainer. It targets Laravel 11/12/13 and PHP 8.2+, which is forward-looking but means it is tracking framework versions that are still moving. Eloquent internals — cast resolution, relationship reflection, trait-provided key types — are among the more brittle parts of Laravel to introspect, and a framework minor release can silently change behavior. The README itself flags a sharp edge: with data_replaces_model enabled, relationships pointing at a replaced model will reference an interface that is no longer generated. That is a compile-time break in the frontend caused by a backend config change, which is precisely the failure mode you do not want in a contract generator. The "142 tests / 344 assertions" figure is respectable but self-reported and small for a tool whose correctness is the entire product.
Key questions.
Attribute casts, computed properties, $appends, enum casts, collection casts?morphTo) relationships, and does it fail loudly or emit any?Stack fit. Requires PHP 8.2+, Laravel 11–13, and a TypeScript frontend. It is indifferent to the frontend framework — the output is plain .ts interfaces — so it fits Vue/Inertia, React, or anything else. The only structural prerequisite is that the frontend consumes Laravel-backed JSON that is shaped like the models, which is the normal case for Inertia apps and for API resources that don't aggressively transform payloads.
Migration path. Greenfield or additive. Nothing must be removed to adopt it. The realistic sequence is: install → publish config → point output_path at the existing hand-written types directory under a distinct filename → run the generator → diff generated interfaces against the hand-written ones. That diff is the value: it surfaces every place where the frontend's assumed types have drifted from the backend's actual models. Where generated output is wrong, custom_props and props_with_types provide corrections without forking. Only after the diff is clean across the models that matter should hand-written interfaces be deleted in favor of the generated ones.
Compatibility. Two compatibility axes matter. First, framework upgrades: because the package introspects Eloquent internals, a Laravel minor bump is a real re-test event, so it should be treated as a dependency to be upgraded deliberately, not floated. Second, the DTO layer: data_replaces_model and data_for_model change the shape of the generated graph, so flipping them is a breaking change to the artifact, not a config tweak. Pin the package version in composer.json rather than accepting caret ranges.
Sequencing.
output_path, excluded_namespaces (every vendor package model that shouldn't leak into frontend types), and discover_related_models. Decide data_replaces_model up front — do not flip it later.php artisan oi:gen-ts into CI followed by a git diff --exit-code on the generated file. This makes the artifact's freshness a build-enforced invariant and catches accidental model changes that were never reflected in the frontend types. Without this step the generated file goes stale within weeks.--watch is a developer convenience; it should never be a deployment mechanism.Maintenance. Low ongoing effort, with one caveat. Regeneration is automatic and cheap; the human cost is in the exceptions — every custom_props entry and every namespace exclusion is a small piece of documentation debt that lives in a config file instead of being self-evident from the models. Budget for a short comment convention in config/oi-laravel-ts.php explaining why each override exists. The larger maintenance question is the dependency itself: a 1-star, single-maintainer package with 0 dependents has no community to absorb a break. Assume we may need to vendor it or fork it, and confirm the MIT license and the small codebase make that cheap — it does.
Support. Self-support only. GitHub issues with one maintainer; no commercial backing, no SLA. That is acceptable for a build-time tool that never touches production traffic and can be pinned and forked, but it is not acceptable to depend on it for anything on the request path. It isn't, which is the saving grace.
Scaling. Scales trivially in runtime terms because it has no runtime. The scaling limit is repo size: one output file for all interfaces means the generated artifact grows monotonically with the model count, and a large domain will produce a multi-thousand-line interfaces.ts that is slow for editors to index and that produces large diffs on every regeneration. Watch for this past roughly 100 models and plan for splitting output by domain if the package doesn't support it (the config shows a single output_path, so this may be a fork-level change).
Failure modes. Ranked by likelihood and impact:
git diff --exit-code gate — this is the single most important operational control.any, or string where number is correct) and TypeScript accepts it. This is the worst case because it defeats the tool's purpose while appearing to work. Mitigation: review the first several generated interfaces by hand, not just the diff.data_replaces_model suppressing a model interface that another interface still references — the README documents this and does not prevent it. Mitigation: don't enable the flag until the DTO coverage is complete, or run tsc --noEmit on the generated file in CI.Ramp-up. Hours, not days. One artisan command and one config file. A mid-level engineer can be productive in an afternoon; the real learning is not the tool but the conventions around it — when to add a custom_prop versus fix the cast in the model, and how the data_* options interact. Those deserve a short internal README before a second engineer touches it.
Bottom line for a TPM. Technically well-suited to the problem, well-documented, and cheap to trial — the spike in step 1 is a few days of work and produces a useful drift report regardless of outcome. The blocker is not technical, it is the 1-star/1-maintainer risk profile, which is mitigated almost entirely by (a) it being build-time only, (b) MIT licensing, and (c) the CI freshness gate. Recommend a timeboxed spike with an explicit evaluation of the diff quality on our hardest models — polymorphic relations, enum casts, and accessor-backed properties — before committing. If the generated output requires more than a handful of custom_props overrides to be correct, the maintenance burden will exceed the value and hand-written types remain the better trade.
How can I help you explore Laravel packages today?