laravel-typescript maintained by frolaxhq
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