Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Ts Publish Laravel Package

abetwothree/laravel-ts-publish

A Laravel package for publishing TypeScript definitions from your application. It streamlines keeping PHP and TypeScript types in sync, reducing manual duplication and helping frontend and backend stay aligned in TypeScript-powered Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

laravel-ts-publish turns your PHP backend into the source of truth for the TypeScript your frontend consumes. You point it at directories, run one Artisan command, and get .d.ts files for models, enums and API resources, plus a laravel-ts-global.ts barrel file the frontend imports from.

  1. Install as a dev dependency — it should never ship to production:
    composer require --dev abetwothree/laravel-ts-publish
    
  2. Publish the config (php artisan vendor:publish and pick the package's config tag) so you get config/ts-publish.php. The keys you will actually touch day to day are models.directories, models.additional_directories, models.included and models.excluded, plus the enum/resource equivalents.
  3. Point it at your code. For a conventional app the defaults (usually app/Models, app/Enums, app/Http/Resources) already cover you. Add extra folders under additional_directories only when models live somewhere unusual, e.g. a module or domain folder.
  4. Run the first publish:
    php artisan ts:publish
    
  5. Look at the output before touching the frontend. Open laravel-ts-global.ts first: it tells you what the package actually resolved, which namespaces exist, and which names collide. Then open one generated model file and one enum to see the shape of what you're about to buy into.
  6. Wire it into tsconfig.json so the generated files are visible to your editor, and add the Vite plugin to your frontend config if you want regeneration/HMR on model changes rather than running the command manually.

First real use case: take a single model with an enum cast and a belongsTo relation, publish it, and import the generated interface into one Vue/React component. That end-to-end slice validates your directories, your namespace config, the global file, and your tsconfig include path — the four things that go wrong on day one.

Implementation Patterns

Full runs vs. targeted runs. Use php artisan ts:publish for a full regeneration (CI, before a release, after migrations). Use --only-resources or --source=<path> for a tight dev loop when you're iterating on one area and don't want to rewrite the whole tree. As of v2.7.0, these partial runs produce the same morphTo union types as a full run — previously you could get a subtly different (wrong) result just because of how you invoked the command. That parity matters: it means a targeted run is now a safe preview, not a separate dialect.

Let related models come along for the ride (v2.7.0). The most day-to-day useful change. If a published model relates to something outside your configured directories — a vendor package model, or Laravel's DatabaseNotification via Notifiable — the package now publishes that model too, so the relation's import actually resolves instead of dangling. The practical consequence:

  • You can delete most of the entries you'd accumulated in models.additional_directories just to make imports compile.
  • Related models published this way get interfaces only — no metadata file, no Vite plugin updates. If you want the full treatment for a specific vendor model, keep it in models.additional_directories. This is the one case where "remove the workaround" is the wrong move.
  • Exclusion still wins: models.excluded, a non-matching models.included, or #[TsExclude] on the class will skip it. If a relation import is still broken, check those three before you go spelunking.

Same-named classes in a union are the flagship fix. Two User models (say App\Models\User and Crm\Models\User), or a UserResource in two namespaces, used to collapse into one class or silently drop a variant in unions, when()/whenNull(), ??, ternaries, morphTo resources and docblock unions like @return User[]|CrmUser[]. Now each gets its own alias. Practical guidance: stop hand-writing discriminated unions on the frontend to compensate for this; regenerate and let the alias do the work. If you do need a stable public name, use #[TsType]/#[TsEnum] on the PHP side rather than post-processing the output.

Use {Model}All for payload types. The …All type aggregates the model with its relations, which is exactly what you want for create/update request shapes and Inertia props. v2.7.0 settles the ambiguity when a relation shares a name with a column or accessor: the relation's type wins in {Model}All. That's the right call for props (you usually want the loaded relation), but it means a property that reads the raw column off the model will be typed as the relation — check any accessor that shadows a relation name.

Attributes are your escape hatches, and they now compose. #[TsType] for a type the package can't infer, #[TsCasts] to override a cast's TS shape, #[TsExtends] to add to a generated interface, #[TsEnum] to rename a colliding enum, #[TsExclude] to drop a class. v2.7.0 fixes two things that made these painful: duplicate imports when an enum is named like another enum's …Type, and import-name clashes between your #[TsType]/#[TsCasts]/#[TsExtends] imports and ones the package injects. If you'd been working around either with manual import edits, remove the workarounds now.

Keep the frontend loop honest. Whether you run the command by hand or lean on the Vite plugin, treat generated files as build artifacts with a deterministic output: run ts:publish in CI and fail on a dirty git diff. That catches "someone added a model and forgot to regenerate" and, just as importantly, surfaces new warnings as reviewable changes rather than as a surprise in a feature PR.

Gotchas and Tips

unknown is deliberate, not a bug. A property typed by a class the package doesn't generate — a service, a DTO, a value object — is now published as unknown rather than an import to a file that doesn't exist. Your build gets quieter while your types get looser. If you were relying on a (broken) import, you'll now see unknown at the use site. Fix it at the source with #[TsType] rather than casting in TypeScript.

Read the new ts:publish warnings — all three of them. v2.7.0 adds warnings for (a) two enums in one namespace publishing the same name, (b) a relation that can't be read on a blank model, (c) a related model that can't be read at all. Warnings mean something was omitted from the output, so the types you get are valid but incomplete:

  • (b) is the sneaky one. Relations depending on request state or auth get left out of the model — no compile error, just a property that mysteriously isn't there. If a relation vanishes from your generated types, this is why.
  • Two enums colliding in one namespace: rename with #[TsEnum]. Don't rely on namespace ordering.
  • Note the broader pattern in this release: relations that throw on read no longer abort ts:publish, and related models whose table doesn't exist are skipped. Resilient, but it means publish can succeed while producing less than you expect. Warnings are the only signal — don't pipe them to /dev/null.

Migrate before you publish. The related-model logic checks whether the table exists. In a CI job that publishes types before running migrations, related models will be skipped and relation imports will break downstream — with only a warning to tell you.

Multi-word namespaces changed shape in the global file. ReportCards is now reportCards; the previous report-cards broke parsing of the entire laravel-ts-global.ts. If you had any workaround for the global file not parsing, or hardcoded the old hyphenated name in frontend imports, that's now dead code and a stale import. Regenerate, then grep the frontend for old casing.

Global name collisions across namespaces are resolved — and now deterministic. A name shared by two namespaces points at the right class. Previously this was the source of "why is my Status the wrong Status" bugs; if you'd been dodging this by renaming PHP classes purely to appease the generator, you can probably undo that.

#[TsCasts] on an EnumResource key used to crash ts:publish outright. If you had a defensive removal of that attribute, restore it.

Vite plugin updates are directory-scoped. Auto-published related models are interfaces only — no metadata file, no plugin entry. If a vendor model's changes aren't triggering regeneration, that's expected; add it to models.additional_directories to opt in. Likewise, anything excluded via #[TsExclude] is excluded from related-model publishing too, which is a convenient footgun if you excluded something for an unrelated reason.

Extension points worth knowing before you patch generated output: #[TsExtends] to add fields, #[TsType] to correct a single property, #[TsCasts] for cast shapes, #[TsEnum] for renames, config included/excluded for scope. Between these, there is almost no reason to hand-edit a generated file — and every hand-edit is a diff conflict waiting to happen on the next ts:publish.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings