kiwilan/laravel-typescriptable
Generate TypeScript types from your Laravel models, resources, and form requests, keeping frontend and backend in sync with automatically updated type definitions and minimal configuration.
1. Install and publish config
composer require kiwilan/laravel-typescriptable --dev
php artisan vendor:publish --provider="Kiwilan\Typescriptable\TypescriptableServiceProvider"
The config lands at config/typescriptable.php. This is the first file to read — it controls output paths, which models/routes/settings get scanned, and any custom type mappings. Default output is typically a resources/js/types (or resources/ts/types) directory; confirm the exact path here before writing any import statements.
2. Discover the actual commands
php artisan list | grep typescriptable
The package exposes generators per concern (models, routes, settings) plus an aggregate "generate everything" command. Run the aggregate once against a real app rather than a toy model — the output shape (namespacing like App.Models.User, relation nesting, optional props) is what you're evaluating, not the install.
3. First use case: type one Inertia prop
Pick a controller that returns Inertia::render('Users/Show', ['user' => $user]). Regenerate, then in the Vue/React page:
import type { App } from '@/types' // adjust to your output path
const props = defineProps<{ user: App.Models.User }>()
That single round-trip tells you whether the package is worth adopting: if the generated interface matches what the API actually serializes (casts, appends, hidden fields), you get real value. If it's off, you'll be fighting it forever.
4. Optional NPM companion
For Inertia projects, install the companion package (check the README for the exact name — it's scoped under @kiwilan/). It provides typed route() helpers, page-prop helpers, and paginator generics. It's strictly optional; the generated .d.ts files are useful on their own.
Wire generation into the build, not into your memory
The single biggest determinant of success is never having to remember to run the generator. Add a script:
// composer.json
"scripts": {
"types": "php artisan typescriptable:generate",
"post-autoload-dump": ["@php artisan package:discover --ansi", "@types"]
}
Or hook it to npm run dev via a predev script so frontend work always starts from fresh types. Stale types are worse than no types — they compile fine and lie to you.
Model interfaces as the contract for Inertia page props
Generate one interface per model, then compose page props from them rather than hand-writing DTOs:
// resources/js/types/pages.d.ts
import type { App } from './index'
declare module '@inertiajs/core' {
interface PageProps {
auth: { user: App.Models.User | null }
flash: { success?: string; error?: string }
}
}
Use this for shared props (auth, flash, settings) and per-page generics for page-specific payloads. Relations come through as nested interfaces, so post.user and post.comments[] type-check without extra work — this is where the package saves the most time.
Routes: type the name and the params
Route generation gives you a union of route names and a params map. The practical win is catching route('posts.show') missing its {post} param at compile time instead of at runtime in production. If the NPM companion's typed route() helper is too invasive, you can still consume the generated name union directly:
type RouteName = App.Routes.RouteName
function go(name: RouteName) { /* ... */ }
Spatie Settings → typed shared props
Settings classes are a natural fit for Inertia shared props. Generate the interface, then share the settings object from HandleInertiaRequests:
public function share(Request $request): array
{
return [
...parent::share($request),
'settings' => GeneralSettings::all(),
];
}
Now frontend feature flags / branding values are typed and refactor-safe on both sides. This is the highest-leverage third of the package for apps that already use Spatie Settings.
Keep generated files in a single barrel
Export everything from one types/index.ts and import via a path alias (@/types). It keeps imports stable when the generator changes its internal file layout between versions — which it will, given the package's maturity level.
Treat generation as a PR-visible diff
Commit the generated files. A types diff in a pull request is a free schema-change review: if someone adds a column and the User interface changes, reviewers see it.
It's a low-star, single-maintainer package. Pin an exact version in composer.json and keep your invocation behind one composer script. If the project stalls, you swap the script's body for spatie/laravel-typescript-transformer or a hand-rolled command without touching a single frontend import — provided you followed the barrel-file advice above.
Generated types are compile-time only. They will not validate a response at runtime. If a controller returns a differently-shaped array than the model, TypeScript happily trusts you and the bug ships. For anything crossing a real network boundary, pair with Zod or keep the model interface scoped to genuinely model-backed payloads.
Casts and DB types don't map 1:1. Expect decimal to surface as string, datetime as string, and JSON columns as loosely-typed any/unknown. Backed PHP enums generally generate the best types — favor them over string constants if you want clean unions on the TS side. Check any $appends or accessor-derived fields manually; computed attributes are the usual source of "the type says number but I get a formatted string."
$hidden / $visible interaction. Confirm against your actual serialization whether hidden attributes are omitted from the generated interface. Mismatches here produce props typed as optional-but-actually-present, which quietly disables strict null checks on that field.
Route params and model binding. Optional route params and implicit model binding ({post} resolved to id vs. object) are the most common source of wrong param types. Verify with a route that has both an optional segment and a bound model before trusting the routes output broadly.
Output path must be inside your tsconfig.json include. A generator writing to resources/js/types while your tsconfig covers only resources/ts is a silent failure — no types, no error, just any. After the first run, confirm the files are actually picked up by opening one in your editor and checking for IntelliSense.
Model name collisions. Two models named the same in different namespaces will collide in the generated output. Check the config for namespace/path scoping options, or rename.
Debugging a bad generation: run the generator with -v, then inspect the specific generated file for the model you care about rather than the whole barrel. If a model is missing entirely, it's almost always a scan-path or namespace-filter issue in config/typescriptable.php — not a bug.
Extension points worth knowing: the config typically allows custom type mappings (PHP type → TS type) and output customization. That's your escape hatch for enums, value objects, and Carbon before you consider forking. Reach for it first; only fork if you need new sources of types (e.g., a custom DTO layer), which the package doesn't cover.
How can I help you explore Laravel packages today?