vincentndegwa/eloquent-typegen

Generate TypeScript types from Eloquent models

Maintainers

Package info

github.com/VincentNdegwa/eloquent-typegen

pkg:composer/vincentndegwa/eloquent-typegen

Statistics

Installs: 32

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.0.5 2026-05-06 15:43 UTC

This package is auto-updated.

Last update: 2026-05-06 17:18:04 UTC


README


⚡ eloquent-typegen

Stop writing TypeScript types by hand. Generate them from your Laravel models.

Reads your $casts, PHP enums, migrations (for nullability), and relationships — and produces accurate .ts files your Vue, React, or Svelte frontend can import immediately.

Installation · Configuration · Usage · Type Mapping · Casting Logic


The Problem

You add a column to your users table. You update the model. You run the migration.

Then you open your frontend and realise:

  • Your TypeScript type still has the old fields
  • You're not sure if score is number | null or just number
  • Your role field is typed as string but it should be 'admin' | 'editor' | 'viewer'
  • Someone added a deleted_at field and now there's a runtime error in production

You shouldn't have to maintain types in two places.

The Solution

One command. Run it after any migration or model change.

php artisan typegen:generate
✓ user.ts
✓ post.ts
✓ blog-post.ts
✓ model-helpers.ts
✓ index.ts

✅  Done! Generated 5 type file(s) → resources/js/types/models

What It Generates

Your Laravel model:

// app/Models/User.php

class User extends Model
{
    use SoftDeletes;

    protected $fillable = ['name', 'email', 'active'];

    protected $hidden = ['password', 'remember_token'];

    protected $casts = [
        'active' => 'boolean',
        'role'   => UserRole::class,
        'score'  => 'decimal:2',
        'meta'   => 'array',
    ];
}

enum UserRole: string
{
    case Admin  = 'admin';
    case Editor = 'editor';
    case Viewer = 'viewer';
}

Your migration:

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email');
    $table->boolean('active')->default(true);
    $table->string('role');
    $table->decimal('score', 8, 2)->nullable(); // ← detected automatically
    $table->json('meta')->nullable();
    $table->timestamps();
    $table->softDeletes();
});

Generated resources/js/types/models/user.ts:

// Auto-generated by eloquent-typegen.
// Run `php artisan typegen:generate` to refresh.

import type { Nullable } from './model-helpers';

export type UserRole = 'admin' | 'editor' | 'viewer';

export interface User {
  readonly id: number;
  name: string;
  email: string;
  active: boolean;
  role: UserRole;
  score?: Nullable<number>;
  meta?: Nullable<Record<string, unknown>>;
  created_at?: Nullable<string>;
  updated_at?: Nullable<string>;
  deleted_at?: Nullable<string>;
}

/** Fields required to create a new User */
export type CreateUserPayload = Omit<User, 'id' | 'created_at' | 'updated_at' | 'deleted_at'>;

/** Fields allowed when updating a User */
export type UpdateUserPayload = Partial<CreateUserPayload>;

Notice what happened automatically:

  • password and remember_token are absent — they're in $hidden
  • score and meta are optional and nullable — read from your migration file
  • role is 'admin' | 'editor' | 'viewer', not just string — derived from your PHP enum
  • deleted_at is included because you use SoftDeletes
  • CreateUserPayload and UpdateUserPayload are generated for free

Installation

Requirements: PHP 8.1+, Laravel 10 / 11 / 12 / 13

Install as a dev dependency — this package has zero production footprint:

composer require vincentndegwa/eloquent-typegen --dev

Laravel's auto-discovery registers the package automatically. No manual config needed.

Optionally publish the config file:

php artisan vendor:publish --tag=typegen-config

Usage

Generate all types

php artisan typegen:generate

Generate for specific models only

php artisan typegen:generate --model=User --model=Post

Preview without writing files

php artisan typegen:generate --dry-run

Custom output path

php artisan typegen:generate --path=src/types/api

Skip relationships

php artisan typegen:generate --no-relations

Using the Generated Types

Vue 3

<script setup lang="ts">
import type { User, UpdateUserPayload } from '@/types/models'

const props = defineProps<{ user: User }>()

async function save(payload: UpdateUserPayload) {
  await $fetch(`/api/users/${props.user.id}`, {
    method: 'PATCH',
    body: payload,
  })
}
</script>

<template>
  <!-- TypeScript knows role is 'admin' | 'editor' | 'viewer' — nothing else compiles -->
  <AdminBadge v-if="user.role === 'admin'" />

  <!-- TypeScript knows score can be null -->
  <span>{{ user.score ?? 'No score yet' }}</span>
</template>

React

import type { User, CreateUserPayload, Paginated } from '@/types/models'

async function getUsers(): Promise<Paginated<User>> {
  const res = await fetch('/api/users')
  return res.json()
}

function UserCard({ user }: { user: User }) {
  return (
    <div>
      <h2>{user.name}</h2>
      {/* 'superadmin' would be a compile error */}
      {user.role === 'admin' && <AdminBadge />}
      {/* TypeScript knows score is number | null */}
      <p>Score: {user.score?.toFixed(2) ?? 'N/A'}</p>
    </div>
  )
}

function CreateUserForm() {
  const [form, setForm] = useState<CreateUserPayload>({
    name: '',
    email: '',
    active: true,
    role: 'viewer',
  })
  // ...
}

Svelte 5

Svelte fully supports TypeScript — add lang="ts" to your <script> block:

<script lang="ts">
  import type { User } from '$lib/types/models'

  let { user }: { user: User } = $props()
</script>

{#if user.role === 'admin'}
  <AdminBadge />
{/if}

<p>Score: {user.score ?? 'No score yet'}</p>

Inertia.js

// Inertia passes your model data as page props — type them directly:
import type { User, Paginated } from '@/types/models'

defineProps<{
  users: Paginated<User>
  auth: { user: User }
}>()

Configuration

config/typegen.php after publishing:

return [

    /*
    |--------------------------------------------------------------------------
    | Model Paths
    |--------------------------------------------------------------------------
    | Directories to scan for Eloquent models. Relative to app_path().
    */
    'model_paths' => ['Models'],

    /*
    |--------------------------------------------------------------------------
    | Output Directory
    |--------------------------------------------------------------------------
    | Where to write .ts files. Relative to base_path() or absolute.
    | The default works for Vite-based projects (Vue, React, Svelte, Inertia).
    */
    'output_path' => 'resources/js/types/models',

    /*
    |--------------------------------------------------------------------------
    | Index File
    |--------------------------------------------------------------------------
    | Generates an index.ts barrel — one import for all your model types.
    */
    'generate_index' => true,

    /*
    |--------------------------------------------------------------------------
    | Helpers File
    |--------------------------------------------------------------------------
    | Generates model-helpers.ts with Nullable<T>, Paginated<T>, ApiError, etc.
    */
    'generate_helpers' => true,

    /*
    |--------------------------------------------------------------------------
    | Date Type
    |--------------------------------------------------------------------------
    | How date/datetime columns are typed. 'string' is the safe default because
    | Laravel serialises dates as ISO strings over the wire.
    */
    'date_type' => 'string', // 'string' | 'Date'

    /*
    |--------------------------------------------------------------------------
    | Excluded Models
    |--------------------------------------------------------------------------
    | Models to skip. Accepts FQCNs or short class names.
    */
    'excluded_models' => [
        // 'App\Models\PersonalAccessToken',
    ],

    /*
    |--------------------------------------------------------------------------
    | Custom Type Map
    |--------------------------------------------------------------------------
    | Override the TypeScript type for specific cast classes.
    */
    'custom_type_map' => [
        // 'App\Casts\Money' => '{ amount: number; currency: string }',
    ],

    /*
    |--------------------------------------------------------------------------
    | Relationships
    |--------------------------------------------------------------------------
    | Include relationship methods as optional properties on generated types.
    */
    'include_relationships' => true,

    /*
    |--------------------------------------------------------------------------
    | Vendor Models
    |--------------------------------------------------------------------------
    | Include vendor models referenced by relations (e.g. notifications).
    */
    'include_vendor_models' => true,

    /*
    |--------------------------------------------------------------------------
    | Additional Models
    |--------------------------------------------------------------------------
    | Explicit model classes to always include in generation.
    */
    'additional_models' => [
      // 'Illuminate\Notifications\DatabaseNotification',
    ],

    /*
    |--------------------------------------------------------------------------
    | Read Migrations
    |--------------------------------------------------------------------------
    | Parse migration files to detect nullable columns accurately.
    | No database connection is required.
    */
    'read_migrations' => true,

];

Type Mapping

PHP / Laravel cast TypeScript type
int, integer, bigInteger number
float, double, decimal, decimal:2 number
bool, boolean boolean
string, char, text, uuid, ulid string
date, datetime, timestamp string (configurable to Date)
immutable_date, immutable_datetime string (configurable to Date)
array, json, object Record<string, unknown>
collection unknown[]
BackedEnum (string-backed) Union of string literals
BackedEnum (int-backed) Union of number literals
UnitEnum Union of case name strings
AsCollection, AsArrayObject unknown[]
AsStringable string
AsEnumCollection:MyEnum MyEnum[]
Custom cast (no toTypeScript()) unknown

Casting Logic

The generator reads these sources in this order:

  1. $casts for field type mapping
  2. $fillable and $dates to discover fields
  3. Migrations for nullable() columns
  4. $hidden to exclude fields
  5. Relationships (optional) to include related types

Custom Casts

Custom casts default to unknown. You can override them in two ways:

  1. Config map in custom_type_map:
// config/typegen.php
'custom_type_map' => [
  'App\Casts\Money' => '{ amount: number; currency: string }',
],
  1. Cast class method by defining a toTypeScript() static method on the cast:
class MoneyCast
{
  public static function toTypeScript(): string
  {
    return '{ amount: number; currency: string }';
  }
}

Add a static toTypeScript() method to any custom cast class and the generator uses it automatically:

class MoneyCast implements CastsAttributes
{
    public static function toTypeScript(): string
    {
        return '{ amount: number; currency: string }';
    }

    // get() and set() ...
}

Or use the config map for third-party casts you can't modify:

'custom_type_map' => [
    'Brick\Money\Money' => '{ amount: number; currency: string }',
],

Generated Helpers

model-helpers.ts ships automatically with every project:

/** Marks a field as possibly null — mirrors Laravel's nullable() */
export type Nullable<T> = T | null

/** A model primary key */
export type ModelId = number

/** Matches Laravel's LengthAwarePaginator JSON output */
export interface Paginated<T> {
  data: T[]
  current_page: number
  last_page: number
  per_page: number
  total: number
  from: number | null
  to: number | null
  first_page_url: string
  last_page_url: string
  next_page_url: string | null
  prev_page_url: string | null
  path: string
  links: { url: string | null; label: string; active: boolean }[]
}

/** Standard Laravel validation error response */
export interface ApiError {
  message: string
  errors?: Record<string, string[]>
}

Automating Generation

Before every Vite build

{
  "scripts": {
    "typegen": "php artisan typegen:generate",
    "dev": "npm run typegen && vite",
    "build": "npm run typegen && vite build"
  }
}

Pre-commit hook (Husky)

# .husky/pre-commit
php artisan typegen:generate
git add resources/js/types/models/

CI — fail if types are stale

- name: Generate TypeScript types
  run: php artisan typegen:generate

- name: Fail if types are out of sync
  run: git diff --exit-code resources/js/types/models/

This fails the pipeline if a developer changed a model and forgot to regenerate types, keeping your team honest without any manual process.

Roadmap

Version Feature
v1.0 Model + migration scanning — everything documented here
v1.1 Zod schema output — generates z.object({...}) alongside .ts types
v1.2 --watch mode — re-generates on model or migration file changes
v2.0 Laravel API Resource scanning — reads toArray() in JsonResource classes to generate types that match exactly what your API returns, not just what the model holds
v2.1 Spatie Laravel Data support
v3.0 Route + controller tracing — per-route types like Wayfinder

v1 is built for projects returning models directly or using simple arrays. v2 is for teams using full API Resource transformations — it's the correct source of truth for API-first Laravel.

FAQ

Does this require a database connection? No. It reads your model classes and migration files from disk — no DB connection needed. Safe to run in CI without any environment setup.

Does it work with Inertia.js? Yes. Inertia passes Laravel model data as page props. Typed props are exactly what this package produces.

What about models that use $guarded = [] instead of $fillable? The generator falls back to migration-detected columns. If you have neither $fillable nor migrations, it generates id and timestamps only. Running with migrations enabled is recommended for these cases.

Can I exclude specific fields from output? Add them to $hidden on the model. Hidden fields are never included in generated types.

What if I use API Resources and only expose some fields? v1 includes all non-hidden model fields. v2 (on the roadmap) fixes this by reading your JsonResource::toArray() directly.

Does Svelte support TypeScript? Yes, fully. Add lang="ts" to your <script> tag. SvelteKit projects ship with TypeScript configured out of the box.

Contributing

Contributions are welcome. To get started locally:

git clone https://github.com/VincentNdegwa/eloquent-typegen.git
cd eloquent-typegen

composer install

composer test       # run the test suite
composer analyse    # PHPStan static analysis
composer format     # Laravel Pint code style

Please write tests for any new behaviour. Open an issue before starting large changes so we can align on approach.

License

MIT — see LICENSE for details.

Built by developers who were tired of TypeScript lying about their Laravel data.

If this saves you time, give it a ⭐ — it helps others find it.