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 Model Typescript Transfomer Laravel Package

nolanos/laravel-model-typescript-transfomer

Generates TypeScript definitions for Laravel Eloquent models via spatie/laravel-typescript-transformer. Maps database column types to TypeScript, respects hidden/visible fields, handles nullable columns, and works with php artisan typescript:generate.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the Spatie foundation first. This package is a thin collector/transformer pair bolted onto spatie/laravel-typescript-transformer; it does nothing on its own. If you don't already have the parent package, install it and publish its config:

composer require spatie/laravel-typescript-transformer
php artisan vendor:publish --tag=typescript-transformer-config

Then install this package — note the package name is misspelled in Composer (transfomer, not transformer), while the PHP namespace is spelled correctly (Nolanos\LaravelModelTypescriptTransformer). Copy-paste from the README rather than retyping:

composer require nolanos/laravel-model-typescript-transfomer

2. Register the two classes in config/typescript-transformer.php:

'collectors' => [
    // ...
    Nolanos\LaravelModelTypescriptTransformer\ModelTypeScriptCollector::class,
],
'transformers' => [
    // ...
    Nolanos\LaravelModelTypescriptTransformer\ModelTransformer::class,
],

3. Confirm the collector is looking at your models. The collector is driven by Spatie's collectors config (the directories / path settings in that same file). If your models live somewhere other than app/Models, this is the first thing to fix — a silently empty generated.d.ts almost always means the directory list is wrong.

4. Generate and inspect:

php artisan typescript:generate

Check output_file in the config (commonly resources/js/types/generated.d.ts) and verify you get something like:

declare namespace App.Models {
  export interface User {
    id: number
    name: string
    email: string
    active: boolean
    created_at: string | null
    updated_at: string | null
  }
}

First use case: pick one model that a frontend screen already consumes (a User, Order, or Product), generate types for it, and replace one hand-written interface on the frontend with App.Models.User. That single swap tells you within an hour whether the schema-derived approach matches how your API actually serializes data — see the gotchas below, because it frequently does not out of the box.

Implementation Patterns

Types come from the database, filtering comes from the model. The transformer reads the table columns and then applies $hidden/$visible. That means your shaping lever is $hidden, not $appends:

class User extends Model
{
    protected $hidden = ['password', 'remember_token', 'two_factor_secret'];
}

Keep $hidden aggressive and treat it as your API contract. Frontend types then only expose what the API is allowed to leak — a nice side effect is that a forgotten $hidden shows up as a visible-to-the-frontend column in the .d.ts diff during review.

Commit the generated file, and diff it in CI. Treat the .d.ts like a lockfile: commit it, and add a CI step that regenerates and fails on a dirty working tree.

php artisan typescript:generate
git diff --exit-code resources/js/types/generated.d.ts

This turns "someone changed a migration and forgot the frontend" into a red build instead of a runtime undefined. It's the single highest-value habit with this package. If you'd rather not commit it, at minimum generate it as an explicit build step before npm run build — never rely on a developer remembering.

Regenerate on the events that change the schema. Wire generation into your workflow so it isn't a manual ritual:

  • composer dump/post-install hook, or a composer types script alongside composer test.
  • A php artisan migrate wrapper or a post-migration listener in local dev.
  • The CI job above, which catches everything else.

Compose relationships yourself. This package emits flat per-model interfaces; it does not walk hasOne/belongsTo/morphTo. Define the composed shapes in a hand-written file that imports from the generated namespace:

// resources/js/types/models.ts
import type { App } from './generated'

export type OrderWithUser = App.Models.Order & { user: App.Models.User }
export type Paginated<T> = { data: T[]; current_page: number; last_page: number }

Keep all hand-written composition in a separate file. If you hand-edit generated.d.ts, the next generation silently wipes it.

Type Inertia props and API clients off the generated interfaces. The natural consumption points are Inertia page props, axios/fetch wrapper return types, and form initial values. For a form, derive the client-side shape rather than duplicating it:

type UserForm = Omit<App.Models.User, 'id' | 'created_at' | 'updated_at'>

Omit/Pick/Partial on the generated interface gets you most of the way to a usable form type without another codegen tool.

Split model namespaces deliberately if you use multiple surfaces. If one model is exposed differently in an admin API vs. a public API, don't fight the single $hidden list with $visible toggling — create a resource/DTO layer for the divergent case and let the generated types cover the canonical shape.

Gotchas and Tips

Generation needs a live, migrated database. Type mapping is column introspection, so typescript:generate cannot run on a fresh checkout without a DB. In CI, point it at SQLite with your migrations applied (DB_CONNECTION=sqlite, php artisan migrate --force) rather than giving the build agent credentials to a real database. Verify behaviour with your real driver before trusting the CI output — column type names differ between MySQL, Postgres, and SQLite, and unknown names fall back to unknown.

Everything is derived from raw columns, so accessors, casts, and $appends are invisible. getFullNameAttribute(), $appends = ['full_name'], protected $casts = ['meta' => 'array'] — none of it appears. meta will be typed as string, not object; dates will be string, not Date; enum casts will be string. Override at the consumption layer with intersection types, or subclass the transformer if the mismatch is systemic:

// app/Support/TypeScript/AppModelTransformer.php
class AppModelTransformer extends \Nolanos\LaravelModelTypescriptTransformer\ModelTransformer
{
    // override the column-type → TS-type mapping for decimal, json, etc.
}

Register the subclass instead of the vendor class in transformers. Do this before you have 80 models depending on the default behaviour — swapping later churns the whole generated file.

decimal/numeric → number is a money bug waiting to happen. Floating-point TypeScript types for currency columns encourage a + b arithmetic on values your backend emits as strings (Laravel serializes decimal:2 casts as strings). Map decimals to string in a subclass, or never touch them arithmetically on the frontend.

Nullability is database-level only. | null comes from NULL in the schema, not from PHP. A non-nullable column that your API conditionally omits, or a nullable column your serializer coalesces, will both lie. Treat | null as "the column is nullable," not "this field may be absent."

Soft deletes and timestamps leak into every interface. deleted_at, created_at, updated_at show up on every model. If your frontend never sees them, add them to a base model's $hidden. Same for pivot — many-to-many data isn't modeled at all, so pivot columns are untyped.

Namespace collisions in App.Models. The output is declare namespace App.Models, which is the generic default. If another Spatie transformer in the same project targets that namespace, interfaces can collide or silently merge. Check the generated file after adding a second collector.

Unknown types are unknown, annotated with the DB type. Scan for these after a schema change — unknown propagates silently and defeats the purpose:

grep -n "unknown" resources/js/types/generated.d.ts

The comment naming the DB type tells you exactly what to add to your mapping table (jsonb, inet, enum, smallint are common misses).

Watch out for SQLite-vs-production drift. A column declared json in a MySQL migration and one declared text in a SQLite run may map differently. Generate against the driver you ship on if the two disagree.

Pin the version and keep the surface small. At ~7 stars and zero dependents, you are effectively the QA team. composer require with an explicit constraint, one wrapper subclass for your type mapping, and one CI diff check keeps the blast radius to "a generated file changed unexpectedly" instead of "the frontend types are subtly wrong and nobody noticed."

Debugging a missing model: check, in order — the collector directories config, whether the model's table actually exists on the connection used at generate time, and whether the class extends Illuminate\Database\Eloquent\Model (custom query builders and non-Eloquent repositories are out of scope entirely). If the file is empty rather than missing entries, it's almost always the directory config.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core