yabx/ts-bundle
yabx/ts-bundle is a PHP package for bundling and managing TypeScript assets in your application. It streamlines TypeScript build integration with straightforward configuration, helping keep frontend assets organized, maintainable, and easier to manage within your development workflow.
Reality check first. yabx/ts-bundle is a Symfony bundle (targeting Symfony 8) with effectively zero ecosystem adoption (2 stars, 0 dependents). It is not a Laravel package. Before you invest any time, confirm what it actually does in your context: it is a TypeScript mapper — it walks PHP class metadata and emits .d.ts/interface definitions so your front end and back end stay in sync. For a Laravel developer, that is still a genuinely useful idea — you just won't get Laravel-native wiring out of the box.
Minimal steps:
Install and inspect, don't wire yet.
composer require yabx/ts-bundle
Then read vendor/yabx/ts-bundle/ — specifically src/ for the mapper/service classes, any DependencyInjection/ extension, and any console command registered. This tells you whether there's a standalone entry point you can call from Laravel, or whether everything is bound to Symfony's container.
Find the generation command. Most bundles of this shape register a bin/console command (something like ts:generate/ts:dump). If one exists, the fastest first use case is: run it once against a single class and see what it emits.
php artisan list | grep -i ts # it almost certainly won't appear
php vendor/bin/... # check for a standalone binary
Start with one DTO, not your whole model layer. Define a plain PHP class with typed properties:
final readonly class UserSummary
{
public function __construct(
public int $id,
public string $name,
public ?string $avatarUrl,
public \DateTimeImmutable $createdAt,
) {}
}
Feed it to the mapper and inspect the output. That single sample tells you how it handles nullables, DateTimeInterface, and constructor promotion — the three things that most often produce wrong TypeScript.
If the Symfony coupling is too tight, don't fight it. Pull the mapper class in isolation (instantiate it directly, bypass the bundle), or use it as a reference implementation and write a 100-line generator around nikic/php-parser or Laravel's ReflectionClass. For a package this small, forking a single service is often cheaper than adapting the bundle.
Where to look first: the mapper service itself (how it resolves property types), the config schema, and the console command. Those three files are the whole package.
DTO-first, not Eloquent-first. The mapper needs clean, typed class metadata. Eloquent models are dynamic magic — $fillable, casts, accessors, relations resolved at runtime — and this bundle has no Eloquent driver. The reliable pattern is to introduce explicit DTOs/Resources that mirror your API responses, map to TypeScript from those, and let your Eloquent models stay out of the contract entirely.
// App\Data\UserResource -> generates `UserResource` TS interface
final readonly class UserResource
{
public function __construct(
public int $id,
public string $email,
public ?string $phone,
public UserStatus $status, // backed enum -> string union
public CarbonImmutable $joinedAt,
) {}
}
Pipeline placement. Generate types as a build step, not a runtime concern:
// package.json
"scripts": {
"types:generate": "php artisan ts:generate",
"dev": "npm run types:generate && vite",
"build": "npm run types:generate && vite build"
}
CI drift guard. The single highest-value pattern with any type generator: in CI, regenerate and fail if the working tree changes. This catches the case where someone edits a DTO and forgets to re-emit types — the whole class of bugs that makes synced types worth having.
- run: php artisan ts:generate
- run: git diff --exit-code resources/js/types/ || (echo "TS types out of date" && exit 1)
Where to put output. Commit generated types to a single directory (resources/js/types/) and never hand-edit them. Add a header banner if the mapper supports templates; if not, a post-generation sed is fine.
Enum mapping. Backed string enums map cleanly to TS string unions or enums. Use them aggressively — they're the mapping that survives refactors best and gives you the most value per line.
Versioning the contract. Treat the generated file as part of your API surface. If you have a public API, tag or snapshot the emitted types alongside releases so downstream consumers can diff.
Front-end consumption. Import the generated interfaces into your Inertia props / fetch layer and let TypeScript enforce the shape at every call site. This is the payoff: rename a DTO property and tsc lights up every consumer.
composer.json's symfony/* requirement. If it pins Symfony 8 components, it may pull in packages that conflict with Laravel's existing Symfony version constraints. Resolve this before committing — do not let a type generator reshape your whole dependency tree.casts(). Feed it DTOs. If you want model-derived types, write a small exporter that converts your models to DTOs first.DateTimeInterface handling. This is the most common source of wrong types. Most mappers emit string, which is correct over JSON, not Date. Verify what yours does; a mismatch here silently produces type-unsound code that compiles fine.?string $foo means string | null (always present, possibly null), not foo?: string. If the mapper conflates them — or if your API omits null fields — your front-end null checks will be wrong. Check emitted output for ?: vs | null.interface references. Keep DTOs flat or ensure the mapper deduplicates by FQCN.array becomes any[] or unknown[] — you lose value shape. Nested generics (Collection<int, UserResource>) frequently degrade. Annotate DTOs with docblocks and check whether the mapper reads @var/@param.Extension; in Laravel you won't have config/packages/*.yaml. Options like output path, namespace, and type overrides likely need to be passed programmatically to the service constructor. Read the Configuration class to find the actual keys — don't guess.DateTime).CarbonImmutable. If none exists, subclassing or forking is the path."yabx/ts-bundle": "1.0.x" or whatever the real tag is), vendor or fork it if it becomes load-bearing, and budget a day to replace it with an in-house generator if it breaks. Given the small surface area, an in-house ReflectionClass-based generator is a realistic fallback and removes the Symfony dependency entirely.ReflectionClass gives you 80% of the value with 0% of the dependency risk.How can I help you explore Laravel packages today?