laravel-ts-sync
v1.1.0
Published
Sync Laravel and TypeScript models bidirectionally. Generate TypeScript interfaces from Laravel models and Laravel DTOs/stubs from TypeScript definitions.
Maintainers
Readme
laravel-ts-sync 🚀
Sync Laravel and TypeScript models bidirectionally 🔄
Generate TypeScript interfaces from Laravel models and Laravel DTOs, form requests, policies, factories, and API resources from TypeScript definitions. Keep your frontend and backend types in perfect harmony! 🤝
Table of Contents 📑
- Features 🌟
- Architecture 🏗️
- Installation 📦
- Quick Start 🚀
- Configuration ⚙️
- CLI Commands 🖥️
- CLI Options 🎛️
- Type Mapping 🔄
- Custom Mappings 🎨
- Generated Files 📄
- Watch Mode 👀
- Programmatic API 🧩
- Advanced Usage 🔧
- Testing 🧪
- Contributing 🤝
- License 📄
- Support 💬
Features 🌟
- 🔀 Bidirectional Sync — Convert Laravel PHP models → TypeScript interfaces AND TypeScript interfaces → Laravel PHP files
- 📋 Full Laravel Support — Parses
$fillable,$guarded,$hidden,$casts, relationships (hasOne,hasMany,belongsTo,belongsToMany,morphTo,morphMany,morphToMany), accessors, mutators, scopes, and enums - 🔧 Generator Options — Generate models, form requests, DTOs, policies, factories, and API resources from TypeScript
- 👀 Watch Mode — Automatically regenerate TypeScript interfaces when Laravel model files change using
chokidar - ⚙️ Highly Configurable — Custom type mappings, field overrides, ignore lists, multiple config file formats (.js, .json, .ts, .rc files)
- 🎨 Interface or Type — Choose between
export interfaceorexport typestyle for TypeScript output - 📁 Per-Model & Aggregated — Generate individual files per model OR a single aggregated
models.tsfile (or both!) - 🧪 Utility Types — Auto-generates
CreateInput,UpdateInput, andFilterutility types for each model - 🏷️ Enum Support — Extracts PHP enums from Laravel models and generates TypeScript enums
- 🔗 Relationship Types — Generates TypeScript types for Eloquent relationships with proper nullability and array handling
- 🧹 Dry Run Mode — Preview what files would be generated without writing anything to disk
- 📝 Validation Rules — Automatically generates Laravel form request validation rules from TypeScript property types
- ✅ Config Validation — Built-in
validatecommand to verify your configuration
Architecture 🏗️
┌─────────────────────────────────────────────────────────────┐
│ laravel-ts-sync CLI │
│ (Commander-based) │
└──────────────────┬──────────────────────────┬──────────────┘
│ │
┌─────────▼──────────┐ ┌──────────▼─────────┐
│ Laravel Parser │ │ TypeScript Parser │
│ (php-parser AST) │ │ (TS Compiler API) │
└─────────┬──────────┘ └──────────┬─────────┘
│ │
┌─────────▼──────────┐ ┌──────────▼─────────┐
│ TypeScript Generator│ │ Laravel Generator │
│ (.ts interfaces, │ │ (.php models, │
│ types, utilities) │ │ requests, DTOs, │
└─────────────────────┘ │ policies, etc.) │
└─────────────────────┘Parsers 🧠
| Parser | Source | Technology | Output |
|--------|--------|------------|--------|
| LaravelParser 🐘 | PHP model files | php-parser (AST) | LaravelModel[] |
| TypeScriptParser 📝 | .ts interface files | TypeScript Compiler API | TypeScriptInterface[] |
Generators 🏭
| Generator | Input | Output |
|-----------|-------|--------|
| TypeScriptGenerator 💙 | LaravelModel[] | .ts interfaces, types, enums, utility types |
| LaravelGenerator 🟠 | TypeScriptInterface[] | .php models, requests, DTOs, policies, factories, API resources |
Installation 📦
Global Installation 🌍
npm install -g laravel-ts-syncLocal Installation (recommended) 📁
npm install --save-dev laravel-ts-syncRun with npx (no install) ⚡
npx laravel-ts-sync --helpRequirements ✅
- Node.js >= 16.0.0
- Laravel project (for Laravel → TypeScript direction)
- TypeScript project (for TypeScript → Laravel direction)
Quick Start 🚀
1️⃣ Initialize Configuration
npx laravel-ts-sync initThis creates a laravel-ts-sync.config.js file in your project root with sensible defaults.
2️⃣ Generate TypeScript from Laravel
npx laravel-ts-sync laravel-to-tsThis parses your Laravel models in ./app/Models and generates TypeScript interfaces in ./generated/.
3️⃣ Generate Laravel from TypeScript
npx laravel-ts-sync ts-to-laravelThis parses your TypeScript interfaces in ./src/types and generates Laravel PHP files in ./generated/.
Configuration ⚙️
Edit laravel-ts-sync.config.js in your project root:
module.exports = {
// 📁 Path to Laravel models directory
laravelPath: './app/Models',
// 📁 Path to TypeScript types directory
typescriptPath: './src/types',
// 📁 Output directory for generated files
outputDir: './generated',
// 📄 Generate one file per model
generatePerModel: true,
// 📄 Generate aggregated types file
generateAggregated: true,
// 📄 Aggregated file name
aggregatedFileName: 'models.ts',
// 🎨 Use 'interface' or 'type' for TypeScript output
interfaceStyle: 'interface',
// 🚫 Models to ignore (by name)
ignoreModels: [],
// 🚫 Fields to ignore (timestamps, etc.)
ignoreFields: ['id', 'created_at', 'updated_at', 'deleted_at'],
// 🔄 Custom type mappings (Laravel type -> TS type)
typeOverrides: {},
// 🎨 Custom field-to-type mappings
customMappings: {},
// ✨ Use Prettier for formatting (requires prettier)
usePrettier: false,
// 👀 Enable watch mode
watchMode: false,
// 🧪 Dry run (no files written)
dryRun: false,
// 📣 Verbose output
verbose: false,
// 🏷️ Generate TypeScript enums from PHP enums
generateEnums: true,
// 🔧 Generate Laravel accessors in model
generateAccessors: false,
// 🔧 Generate Laravel mutators in model
generateMutators: false,
// 🔍 Generate Laravel query scopes
generateScopes: false,
// 🛡️ Generate Laravel policies
generatePolicies: false,
// 🏭 Generate Laravel factories
generateFactories: false,
// 📡 Generate Laravel API resources
generateApiResources: false,
};Config File Formats 📄
The config loader supports multiple formats (searched in order):
| Format | File Name |
|--------|-----------|
| JavaScript | laravel-ts-sync.config.js |
| JSON | laravel-ts-sync.config.json |
| TypeScript | laravel-ts-sync.config.ts |
| RC (JSON) | .laravel-ts-syncrc |
| RC (JSON5) | .laravel-ts-syncrc.json |
| Package.json | laravelTsSync key in package.json |
CLI Commands 🖥️
laravel-ts-sync generate 📝
Generate files based on direction:
# Laravel to TypeScript 🔀
npx laravel-ts-sync generate --direction laravel-to-ts
# TypeScript to Laravel 🔀
npx laravel-ts-sync generate --direction ts-to-laravellaravel-ts-sync laravel-to-ts (alias: l2ts) 💙
Generate TypeScript interfaces from Laravel models:
npx laravel-ts-sync laravel-to-ts
# With options
npx laravel-ts-sync laravel-to-ts --output ./src/types --verbose --dry-run
# Ignore specific models
npx laravel-ts-sync laravel-to-ts --ignore User,OldModellaravel-ts-sync ts-to-laravel (alias: ts2l) 🟠
Generate Laravel files from TypeScript interfaces:
npx laravel-ts-sync ts-to-laravel
# With options
npx laravel-ts-sync ts-to-laravel --output ./app/Models --dry-run --verboselaravel-ts-sync init 🏁
Create a configuration file:
npx laravel-ts-sync initlaravel-ts-sync watch 👀
Watch for changes and regenerate:
npx laravel-ts-sync watch
# With custom config
npx laravel-ts-sync watch --config ./config/laravel-ts-sync.config.jslaravel-ts-sync validate ✅
Validate configuration file:
npx laravel-ts-sync validate
# Validate a specific config
npx laravel-ts-sync validate --config ./custom-config.jsCLI Options 🎛️
| Option | Alias | Description |
|--------|-------|-------------|
| --config <path> | -c | Path to config file |
| --output <path> | -o | Output directory |
| --dry-run | | Run without writing files |
| --verbose | -v | Enable verbose output |
| --ignore <models> | | Models to ignore (comma-separated, e.g. User,Post) |
| --direction <direction> | -d | Generation direction: laravel-to-ts or ts-to-laravel |
| --help | -h | Display help information |
| --version | -V | Display version number |
Type Mapping 🔄
Laravel to TypeScript 🐘 → 💙
| Laravel Type | TypeScript Type | Notes |
|-------------|----------------|-------|
| int | number | |
| integer | number | |
| bigint | number | |
| float | number | |
| double | number | |
| decimal | number | |
| string | string | |
| varchar | string | |
| char | string | |
| text | string | |
| longtext | string | |
| mediumtext | string | |
| tinytext | string | |
| boolean | boolean | |
| bool | boolean | |
| date | string | ⚠️ Use string for serialized dates |
| datetime | string | ⚠️ Use string for serialized dates |
| timestamp | string | ⚠️ Use string for serialized dates |
| time | string | |
| json | Record<string, unknown> | |
| array | unknown[] | |
| object | Record<string, unknown> | |
| binary | Buffer | |
| uuid | string | |
| enum | string | ⚠️ Use generateEnums for typed enums |
| file | File | |
| collection | unknown[] | |
TypeScript to Laravel 💙 → 🐘
| TypeScript Type | Laravel Type | Notes |
|----------------|-------------|-------|
| number | integer | |
| string | string | |
| boolean | boolean | |
| Date | datetime | |
| Buffer | binary | |
| Record<string, unknown> | json | |
| unknown[] | json | |
| File | file | |
Relationship Type Mapping 🔗
| Laravel Relation | TypeScript Type | Nullable |
|-----------------|----------------|----------|
| hasOne | RelatedModel \| null | ✅ |
| hasMany | RelatedModel[] | ❌ |
| belongsTo | RelatedModel \| null | ✅ |
| belongsToMany | RelatedModel[] | ❌ |
| morphTo | RelatedModel \| null | ✅ |
| morphMany | RelatedModel[] | ❌ |
| morphToMany | RelatedModel[] | ❌ |
Custom Mappings 🎨
Add custom mappings in your config to handle project-specific types:
module.exports = {
// 🎨 Custom field name -> Laravel cast type mappings
customMappings: {
'Money': 'decimal:10,2',
'Status': 'enum:active,inactive',
'UUID': 'uuid',
'SocialLinks': 'json',
'Coordinates': 'object',
},
// 🔄 Override specific field types
typeOverrides: {
'amount': 'number',
'currency': 'string',
'metadata': 'Record<string, unknown>',
'tags': 'string[]',
},
// 🚫 Ignore specific fields globally
ignoreFields: [
'id',
'created_at',
'updated_at',
'deleted_at',
'laravel_through_key',
'remember_token',
],
};Generated Files 📄
TypeScript Output 💙
When running laravel-to-ts, each model generates:
Main Interface
// models/user.ts
export interface User {
id: number;
name: string;
email: string;
password?: string;
is_active: boolean;
role: 'admin' | 'user' | 'guest';
avatar_url?: string;
bio?: string | null;
date_of_birth?: string;
email_verified_at?: string;
created_at: string;
updated_at: string;
}Utility Types (auto-generated) 🛠️
// 📝 Create input — all fillable fields required
export type UserCreateInput = {
name: string;
email: string;
password: string;
role?: string;
is_active?: boolean;
avatar_url?: string;
bio?: string | null;
};
// ✏️ Update input — all fields optional
export type UserUpdateInput = {
name?: string;
email?: string;
password?: string;
role?: string;
is_active?: boolean;
avatar_url?: string;
bio?: string | null;
date_of_birth?: string;
};
// 🔍 Filter type — all fields optional for query filtering
export type UserFilter = {
id?: number;
name?: string;
email?: string;
role?: string;
is_active?: boolean;
created_at?: string;
updated_at?: string;
};Generated Enums 🏷️
// Enums for User
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}Relation Types 🔗
import type { Post } from './post';
import type { Profile } from './profile';
export interface User {
// ...fields...
posts?: Post[];
profile?: Profile | null;
}
// Relation types
export type UserPostsRelation = Post;
export type UserProfileRelation = Profile;Laravel Output 🟠
When running ts-to-laravel, each interface generates:
Eloquent Model 🐘
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
use HasFactory;
protected $table = 'users';
protected $fillable = [
'name', 'email', 'password', 'role',
'is_active', 'avatar_url', 'bio', 'date_of_birth'
];
protected $casts = [
'is_active' => 'boolean',
'email_verified_at' => 'datetime',
'date_of_birth' => 'datetime',
'settings' => 'array',
];
protected $hidden = ['password'];
}Form Request ✅
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255'],
'password' => ['required', 'string', 'min:8'],
'role' => ['nullable', 'string'],
'is_active' => ['nullable', 'boolean'],
'avatar_url' => ['nullable', 'string', 'url'],
'bio' => ['nullable', 'string', 'max:1000'],
'date_of_birth' => ['nullable', 'date'],
];
}
public function messages(): array
{
return [
// Add custom validation messages here
];
}
}DTO 📦
<?php
namespace App\Dtos;
class UserDto
{
private string $name;
private string $email;
private string $password;
private ?string $role;
private ?bool $is_active;
private ?string $avatar_url;
private ?string $bio;
private ?string $date_of_birth;
public function __construct(
string $name,
string $email,
string $password,
?string $role = null,
?bool $is_active = null,
?string $avatar_url = null,
?string $bio = null,
?string $date_of_birth = null
) {
$this->name = $name;
$this->email = $email;
$this->password = $password;
$this->role = $role;
$this->is_active = $is_active;
$this->avatar_url = $avatar_url;
$this->bio = $bio;
$this->date_of_birth = $date_of_birth;
}
// Getters...
public function getName(): string { return $this->name; }
// toArray()...
public function toArray(): array { /* ... */ }
// fromArray()...
public static function fromArray(array $data): self { /* ... */ }
}Policy 🛡️
Generated when generatePolicies: true
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool { return true; }
public function view(User $user, User $model): bool { return true; }
public function create(User $user): bool { return true; }
public function update(User $user, User $model): bool { return $user->id === $model->id; }
public function delete(User $user, User $model): bool { return $user->id === $model->id; }
public function restore(User $user, User $model): bool { return true; }
public function forceDelete(User $user, User $model): bool { return true; }
}Factory 🏭
Generated when generateFactories: true
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => bcrypt('password'),
'role' => fake()->randomElement(['admin', 'user', 'guest']),
'is_active' => fake()->boolean(),
'bio' => fake()->optional()->text(200),
];
}
}API Resource 📡
Generated when generateApiResources: true
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => $this->role,
'is_active' => $this->is_active,
'avatar_url' => $this->avatar_url,
'bio' => $this->bio,
'date_of_birth' => $this->date_of_birth,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}Watch Mode 👀
Enable automatic regeneration on file changes:
npx laravel-ts-sync watchThis will watch the laravelPath directory and regenerate TypeScript interfaces whenever a model file is created, modified, or deleted. 🪄
# With verbose output
npx laravel-ts-sync watch --verbose
# With custom config
npx laravel-ts-sync watch --config ./my-config.jsThe watcher uses chokidar with these defaults:
- Ignores dotfiles
- Watches all PHP files in the configured
laravelPathdirectory - Automatically debounces rapid successive changes
- Press
Ctrl+Cto stop watching
Programmatic API 🧩
You can also use laravel-ts-sync programmatically in your own scripts:
import { LaravelParser, TypeScriptGenerator, loadConfig, resolvePaths } from 'laravel-ts-sync';
async function generate() {
// 1️⃣ Load configuration
const config = loadConfig('./laravel-ts-sync.config.js');
const paths = resolvePaths(config);
// 2️⃣ Parse Laravel models
const parser = new LaravelParser(config);
const models = await parser.parseDirectory(paths.laravelPath);
// 3️⃣ Generate TypeScript interfaces
const generator = new TypeScriptGenerator(config);
const result = await generator.generateFromModels(models, paths.outputDir);
console.log(`✅ Generated ${result.filesGenerated.length} files`);
console.log(`⏱️ Duration: ${result.duration}ms`);
}Available Classes 📚
| Class | Import | Description |
|-------|--------|-------------|
| LaravelParser | laravel-ts-sync | Parses PHP Laravel model files into LaravelModel[] |
| TypeScriptParser | laravel-ts-sync | Parses TypeScript interface files into TypeScriptInterface[] |
| TypeScriptGenerator | laravel-ts-sync | Generates .ts files from LaravelModel[] |
| LaravelGenerator | laravel-ts-sync | Generates .php files from TypeScriptInterface[] |
Available Utilities 🔧
| Function | Description |
|----------|-------------|
| loadConfig(path?) | Load and merge config from file with defaults |
| saveConfig(config) | Save config to default file path |
| validateConfig(config) | Validate config, returns string[] of errors |
| resolvePaths(config) | Resolve relative paths to absolute |
| createDefaultConfig() | Create config with all default values |
Advanced Usage 🔧
Example: Full Laravel Model with Relationships
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Post extends Model
{
protected $fillable = ['title', 'content', 'user_id', 'status', 'published_at'];
protected $casts = [
'published_at' => 'datetime',
'is_pinned' => 'boolean',
'metadata' => 'array',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}Generated TypeScript
import type { User } from './user';
import type { Comment } from './comment';
export interface Post {
id: number;
title: string;
content: string;
user_id: number;
status: string;
published_at?: string;
is_pinned: boolean;
metadata: Record<string, unknown>;
created_at: string;
updated_at: string;
user?: User | null;
comments?: Comment[];
}
export type PostCreateInput = {
title: string;
content: string;
user_id: number;
status: string;
published_at?: string;
is_pinned?: boolean;
metadata?: Record<string, unknown>;
};
export type PostFilter = {
id?: number;
title?: string;
user_id?: number;
status?: string;
is_pinned?: boolean;
};Using interfaceStyle: 'type' 🎨
module.exports = {
interfaceStyle: 'type', // Use 'type' instead of 'interface'
};This generates:
export type User = {
id: number;
name: string;
email: string;
// ...
};Testing 🧪
# Run tests 🏃
npm test
# Run tests in watch mode 👀
npm run test:watch
# Run tests with coverage 📊
npm run test:coverage
# Lint source code 🔍
npm run lint
# TypeScript type checking ✅
npm run typecheckContributing 🤝
We welcome contributions! Here's how you can help:
- 🍴 Fork the repository
- 🌿 Create your feature branch (
git checkout -b feature/amazing-feature) - 💻 Commit your changes (
git commit -m 'Add some amazing feature') - 📤 Push to the branch (
git push origin feature/amazing-feature) - 🔁 Open a Pull Request
Development Setup 🛠️
# Clone the repository
git clone https://github.com/yourusername/laravel-ts-sync.git
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm testCode Style Guidelines 📐
- Follow the existing code style and conventions
- Write tests for new features
- Update documentation as needed
- Use meaningful commit messages
- Keep pull requests focused on a single change
License 📄
This project is licensed under the MIT License — see the LICENSE file for details.
Support 💬
- 🐛 GitHub Issues — Report bugs and request features
- 📖 Documentation — Full documentation
- 💬 Discussions — Ask questions and share ideas
