frolaxhq / laravel-typescript
Generate TypeScript interfaces from your Laravel Eloquent models
Fund package maintenance!
Requires
- php: ^8.3
- composer/class-map-generator: ^1.4
- illuminate/contracts: ^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^9.0.0|^10.0.0|^11.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
README
Generate TypeScript interfaces from your Eloquent models automatically. Keep your frontend types in sync with your database schema and model definitions.
📚 Read the Full Documentation
✨ Features
- Automatic Model Discovery — Automatically finds models in your codebase (PSR-4) without manual path config
- Automatic type generation — Scans your models and generates TypeScript interfaces/types
- Full type resolution — Precedence chain: overrides → docblocks → accessors → casts → DB types
- Import-aware overrides — Define external TypeScript symbols per field with automatic import dedup
- Relation support — Deep support for all Eloquent relations, counts, exists, and sums
- Enum support — Generate const objects, TypeScript enums, or union types
- Standalone Types — Define custom TypeScript interfaces in your config
- API Resources — Optional
{ data: T }response wrappers - Per-model files — One file per model with barrel export, or single bundled file
- Incremental builds — Intelligent caching for fast generation
- Formatter integration — Auto-format with Prettier or Biome
Requirements
- PHP 8.2+
- Laravel 11 or 12
Installation
composer require frolax/laravel-typescript --dev
Publish the configuration:
php artisan vendor:publish --tag=typescript-config
Quick Start
Generate TypeScript definitions:
php artisan typescript:generate
Output to stdout:
php artisan typescript:generate --stdout
Generate for a specific model:
php artisan typescript:generate User
Output Example
Given this Eloquent model:
class User extends Model { protected $fillable = ['name', 'email']; protected $hidden = ['password']; protected $casts = [ 'email_verified_at' => 'datetime', 'role' => UserRole::class, ]; public function posts(): HasMany { return $this->hasMany(Post::class); } }
The generated TypeScript:
// This file is auto-generated by laravel-typescript. // Do not edit this file manually. export interface User { // Columns id: number; name: string; email: string; email_verified_at: string | null; role: UserRole; password: string; created_at: string | null; updated_at: string | null; // Relations posts: Post[]; // Counts posts_count?: number; } export const UserRole = { Admin: 'admin', User: 'user', } as const; export type UserRole = typeof UserRole[keyof typeof UserRole];
CLI Commands
typescript:generate
Generate TypeScript definitions from Eloquent models.
php artisan typescript:generate [model] [options]
| Option | Description |
|---|---|
model |
Generate for a specific model only |
--output=PATH |
Output path (overrides config) |
--writer=TYPE |
Writer: interface, type, or json |
--enum-style=STYLE |
Enum style: const_object, ts_enum, union |
--global |
Wrap in declare namespace |
--plurals |
Pluralize type names (User → Users) |
--fillables |
Generate fillable-only types |
--no-relations |
Exclude relations |
--no-counts |
Exclude relation counts |
--timestamps-as-date |
Map timestamps to Date instead of string |
--optional-nullables |
Make nullable columns optional (name?: type) |
--connection=NAME |
Database connection to use |
--strict |
Bail on first model error |
--stdout |
Output to stdout only |
typescript:inspect
Inspect a model's metadata:
php artisan typescript:inspect "App\Models\User" php artisan typescript:inspect "App\Models\User" --json
typescript:mappings
Show current type mappings:
php artisan typescript:mappings
Configuration
All options are documented in config/typescript.php. Key sections:
Discovery
'discovery' => [ 'auto_discover' => true, // Automatically find models in your codebase 'paths' => [app_path('Models')], // Add extra paths if needed 'excluded_models' => ['BaseModel'], ],
Output
'output' => [ 'path' => resource_path('types/generated'), 'per_model_files' => true, // One file per model 'barrel_export' => true, // Generate index.ts 'enum_directory' => 'enums', // Subdirectory for enums ],
Writer
'writer' => [ 'default' => 'interface', // 'interface', 'type', or 'json' 'enum_style' => 'const_object', // 'const_object', 'ts_enum', 'union' 'global_namespace' => null, // Wrap in declare namespace 'fillable_types' => false, // Generate UserFillable types ],
Relations
'relations' => [ 'enabled' => true, 'optional' => false, // Make all relations optional 'counts' => ['enabled' => true, 'optional' => true], 'exists' => ['enabled' => true, 'optional' => true], ],
Custom Type Mappings
'mappings' => [ 'custom' => [ 'point' => '{ lat: number; lng: number }', 'money' => 'string', ], 'timestamps_as_date' => false, ],
Naming Convention
'case' => [ 'columns' => 'camel', // 'snake', 'camel', 'pascal' 'relations' => 'camel', ],
Formatter
'formatter' => [ 'enabled' => true, 'tool' => 'prettier', // 'prettier' or 'biome' ],
Incremental Builds
'cache' => [ 'enabled' => true, // Skip unchanged models ],
Extension API
Use the Typescript facade to extend the package:
Custom Type Mappers
use Frolax\Typescript\Facades\Typescript; Typescript::extend(function ($registry) { $registry->registerMapper(new class implements TypeMapperContract { public function supports(string $type): bool { return $type === 'point'; } public function resolve(string $type): string { return '{ lat: number; lng: number }'; } }); });
Forced Type Overrides
Use the $interfaces property on your model:
class User extends Model { public array $interfaces = [ 'metadata' => 'Record<string, unknown>', 'settings' => 'UserSettings', 'attachments' => [ 'type' => 'MessagePartAttachment[]', 'import' => '@/types/ai', ], 'avatar' => [ 'type' => 'ImageAsset', 'nullable' => true, 'import' => '@/types/media', ], ]; }
String form and object form can be mixed in the same model.
When using import, generated files include import type statements automatically.
If multiple fields reference the same symbol/path pair, it is imported only once per output file.
Example generated import block:
import type { ImageAsset } from '@/types/media'; import type { MessagePartAttachment } from '@/types/ai';
Architecture
The package uses a pipeline architecture:
Discovery → Introspection → Metadata → Type Resolution → Relation Resolution → Writing → Formatting
Each stage has a clear contract and can be replaced or extended:
| Module | Contract | Default |
|---|---|---|
| Discovery | ModelDiscoveryContract |
ModelDiscovery |
| Introspection | SchemaIntrospectorContract |
LaravelSchemaIntrospector |
| Metadata | ModelMetadataExtractorContract |
ModelMetadataExtractor |
| Type Resolution | TypeResolverContract |
TypeResolver |
| Relations | RelationResolverContract |
RelationResolver |
| Writing | WriterContract |
TypescriptWriter |
| Formatting | FormatterContract |
NullFormatter |
Type Resolution Precedence
Types are resolved using an 8-level precedence chain:
- Forced override —
$interfacesproperty on model - API resource type — When
api_resourcesmode is enabled - Enum cast —
AsEnumCollection,AsEnumArrayObject - Accessor return type — PHP return type from accessor method
- Cast type — Eloquent
$castsproperty - DB column type — Raw database schema type
- Custom user mapping — From
config('typescript.mappings.custom') - Fallback —
unknown
Testing
composer test
The test suite includes 135+ tests covering:
- Unit tests for all components (mappers, resolvers, writers, cache)
- Integration tests (schema introspection, metadata extraction)
- E2E pipeline tests (full generation flow)
- Artisan command tests
License
MIT