campelo / laravel-typescript-models
Automatically generate TypeScript interfaces from your Laravel Eloquent models via API endpoint
Package info
github.com/campeloneto1/laravel-typescript-models
pkg:composer/campelo/laravel-typescript-models
Requires
- php: ^8.1
- illuminate/filesystem: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0|^11.0
This package is auto-updated.
Last update: 2026-03-13 16:34:42 UTC
README
Automatically generate TypeScript interfaces from your Laravel Eloquent models, API Resources, and Form Requests via an API endpoint or CLI command.
Features
- API Resources (Recommended): Generate interfaces from JsonResource classes as source of truth
- Models: Generate TypeScript interfaces from Eloquent models (opt-in)
- Form Requests: Generate interfaces from FormRequest validation rules
- Yup Schemas: Generate Yup validation schemas from Form Requests
- Zod Schemas: Generate Zod validation schemas from Form Requests
- Intelligent Type Inference: Eliminate
anytypes using PHPDoc, static analysis, and Model casts - Split by Domain: Generate separate files per module (
users.ts,orders.ts) - Union Literal Types: Enum-like types from
in:rules ('admin' | 'user' | 'guest') - Pagination Types: Auto-generated
PaginatedResponse<T>generic type - Array Types: Auto-generated array types (
Users,Posts, etc.) - Smart Detection: Multiple strategies for type detection (return type, PHPDoc, method body)
- Conflict Resolution: Automatic name prefixing when classes have the same name in different folders
- CLI Command:
php artisan typescript:generatefor CI/CD integration - Web Configurator: Interactive HTML page to customize and download types
- Security: Token authentication, IP whitelist, disabled by default
Installation
composer require campelo/laravel-typescript-models
The package will automatically register its service provider.
Configuration
Publish the configuration file:
php artisan vendor:publish --tag=typescript-models-config
Environment Variables
# Enable the endpoint (disable in production!) TYPESCRIPT_MODELS_ENABLED=true # Secret token for authentication TYPESCRIPT_MODELS_TOKEN=your-secret-token-here # Optional: Custom route path TYPESCRIPT_MODELS_ROUTE=/api/typescript-models # Optional: Allowed IPs (comma-separated) TYPESCRIPT_MODELS_ALLOWED_IPS=127.0.0.1,::1 # Optional: Properties mode (fillable, database, or both) TYPESCRIPT_MODELS_PROPERTIES_MODE=fillable # Optional: Include/exclude features TYPESCRIPT_MODELS_INCLUDE_MODELS=false # Models disabled by default (use Resources!) TYPESCRIPT_MODELS_INCLUDE_ACCESSORS=false TYPESCRIPT_MODELS_INCLUDE_RELATIONS=true TYPESCRIPT_MODELS_INCLUDE_RESOURCES=true TYPESCRIPT_MODELS_INCLUDE_REQUESTS=true TYPESCRIPT_MODELS_GENERATE_YUP_SCHEMAS=true TYPESCRIPT_MODELS_GENERATE_ZOD_SCHEMAS=false # Type inference for Resources (eliminates 'any' types) TYPESCRIPT_MODELS_INFER_TYPES=true TYPESCRIPT_MODELS_UNKNOWN_FALLBACK=unknown # 'unknown' | 'any' | 'never' # Split output by domain TYPESCRIPT_MODELS_SPLIT_BY_DOMAIN=false # 'subdirectory' | 'class' | false TYPESCRIPT_MODELS_DOMAIN_DETECTION=subdirectory
Usage
CLI Command (Recommended)
Generate TypeScript interfaces directly from the command line:
# Generate with default options (Resources + Requests, no Models) php artisan typescript:generate # Specify output file php artisan typescript:generate --output=resources/js/types/api.d.ts # Control what to generate with --only and --include php artisan typescript:generate --only=resources # Only resources php artisan typescript:generate --only=resources,requests # Resources + Requests php artisan typescript:generate --include=models # Add models to default # Legacy flags (still supported) php artisan typescript:generate --models # Only models php artisan typescript:generate --resources # Only resources php artisan typescript:generate --requests # Only requests # Choose validation schema library php artisan typescript:generate --yup # Generate Yup schemas php artisan typescript:generate --zod # Generate Zod schemas php artisan typescript:generate --yup --zod # Generate both # Split by domain (generates multiple files) php artisan typescript:generate --split-by=subdirectory --output=resources/types/ # Creates: users.ts, orders.ts, _shared.ts, index.ts # Exclude pagination/array types php artisan typescript:generate --no-paginated --no-array-types
Web Configurator
Access the interactive configurator page to customize and download your types:
http://localhost/api/typescript-models/configurator?token=your-secret-token
Features:
- Toggle models, resources, requests
- Enable/disable relations and accessors
- Choose between Yup and Zod schemas
- Preview before download
- Copy generated URL for CI/CD
API Endpoint
# Using token in header curl -H "X-TypeScript-Token: your-secret-token" http://localhost/api/typescript-models # Using Bearer token curl -H "Authorization: Bearer your-secret-token" http://localhost/api/typescript-models # Using query parameter curl "http://localhost/api/typescript-models?token=your-secret-token" # With custom options curl "http://localhost/api/typescript-models?token=your-token&resources=1&requests=1&yup=1&zod=0"
Integrating with Your Frontend
// package.json { "scripts": { "types:generate": "curl -H 'X-TypeScript-Token: your-token' http://localhost/api/typescript-models > src/types/models.d.ts" } }
Resources vs Models: Best Practices
Resources are the recommended source of truth for frontend types because:
- They represent the actual API response shape
- They hide internal fields that shouldn't be exposed
- Multiple Resources can exist for the same Model (summary, detail, admin)
By default, Models are disabled (include_models=false). Enable them only if needed:
# Default: Only Resources + Requests php artisan typescript:generate # Include Models for admin panels or internal tools php artisan typescript:generate --include=models # Or via config TYPESCRIPT_MODELS_INCLUDE_MODELS=true
Split by Domain
For large codebases, split generated types into separate files:
php artisan typescript:generate --split-by=subdirectory --output=resources/types/
Directory Structure
Given this Resource structure:
app/Http/Resources/
├── Users/
│ ├── UserResource.php
│ └── UserSummaryResource.php
├── Orders/
│ └── OrderResource.php
└── ProductResource.php (no subdirectory)
Generates:
resources/types/
├── _shared.ts # PaginatedResponse, PaginationLink
├── index.ts # Re-exports everything
├── users.ts # UserResource, UserSummaryResource
├── orders.ts # OrderResource
└── default.ts # ProductResource (no subdirectory)
Generated Files
_shared.ts
export interface PaginatedResponse<T> { ... } export interface PaginationLink { ... }
users.ts
import type { PaginatedResponse } from './_shared'; export interface UserResource { ... } export interface UserSummaryResource { ... } export type UserResources = UserResource[]; export type UserResourcesPaginated = PaginatedResponse<UserResource>;
index.ts
export * from './_shared'; export * from './users'; export * from './orders'; export * from './default';
Detection Modes
| Mode | Description | Example |
|---|---|---|
subdirectory |
Uses namespace subdirectory | Resources\Users\ → users.ts |
class |
Groups by matching Model names | UserResource, StoreUserRequest → user.ts |
Class Mode (Entity Matching)
The class mode intelligently groups interfaces by matching against your discovered Models:
php artisan typescript:generate --split-by=class --output=resources/types/
How it works:
- Discovers all your Eloquent Models (
User,Post,PostLike, etc.) - For each Resource/Request, removes suffixes (
Resource,Request) and HTTP verb prefixes (Store,Update,Delete,Index,Show,Create,Destroy) - Matches the clean name against Model names (exact match or plural match)
Examples:
| Class | Clean Name | Matched Model | Output File |
|---|---|---|---|
UserResource |
User |
User |
user.ts |
StoreUserRequest |
User |
User |
user.ts |
DeletePostLikeRequest |
PostLike |
PostLike |
post_like.ts |
PostLikesResource |
PostLikes |
PostLike (plural) |
post_like.ts |
TripUserResource |
TripUser |
TripUser |
trip_user.ts |
Note: The matching is exact to avoid conflicts. For example, with Models
User,Trip, andTripUser, the classTripUserRequestwill correctly matchTripUser(notUserorTrip).
1. Model Interfaces
Generate TypeScript interfaces from your Eloquent models.
Laravel Model
// app/Models/User.php class User extends Model { protected $fillable = ['name', 'email', 'birth_date']; protected $casts = [ 'birth_date' => 'date', ]; public function posts(): HasMany { return $this->hasMany(Post::class); } public function profile(): HasOne { return $this->hasOne(Profile::class); } }
Generated TypeScript
// Model Interfaces export interface User { id: number; name?: string; email?: string; birth_date?: Date; created_at?: Date; updated_at?: Date; posts?: Post[]; profile?: Profile; } export interface Post { id: number; title?: string; content?: string; user_id?: number; created_at?: Date; updated_at?: Date; user?: User; } // Model Array Types export type Users = User[]; export type Posts = Post[]; // Model Paginated Types export type UsersPaginated = PaginatedResponse<User>; export type PostsPaginated = PaginatedResponse<Post>;
Relationship Detection
The package uses 3 strategies to detect relationships:
- Return Type (Recommended)
public function posts(): HasMany { return $this->hasMany(Post::class); }
- PHPDoc Annotation
/** @return HasMany */ public function posts() { return $this->hasMany(Post::class); }
- Method Body Analysis
public function posts() { return $this->hasMany(Post::class); // Auto-detected! }
Supported Relationships
| Relationship | TypeScript Type |
|---|---|
HasOne, BelongsTo, MorphOne, MorphTo, HasOneThrough |
RelatedModel |
HasMany, BelongsToMany, MorphMany, MorphToMany, HasManyThrough |
RelatedModel[] |
2. API Resource Interfaces
Generate interfaces from your API Resources. Useful when your API response differs from the model structure.
Laravel Resources
// app/Http/Resources/UserResource.php class UserResource extends JsonResource { public function toArray($request) { return [ 'id' => $this->id, 'full_name' => $this->name, 'email' => $this->email, 'avatar_url' => $this->getAvatarUrl(), 'member_since' => $this->created_at->format('Y-m-d'), ]; } } // app/Http/Resources/UserSummaryResource.php class UserSummaryResource extends JsonResource { public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, ]; } } // app/Http/Resources/UserProfileResource.php class UserProfileResource extends JsonResource { public function toArray($request) { return [ 'id' => $this->id, 'full_name' => $this->name, 'email' => $this->email, 'bio' => $this->profile->bio, 'posts_count' => $this->posts->count(), ]; } }
Generated TypeScript (with Intelligent Type Inference)
// Resource Interfaces - No more 'any' types! export interface UserResource { avatar_url?: string; // Inferred from name pattern (*_url) email?: string; // Inferred from name pattern full_name?: string; // Inferred from name pattern id?: number; // Inferred from name pattern member_since?: string; // Inferred from ->format() call } export interface UserSummaryResource { id?: number; name?: string; } export interface UserProfileResource { bio?: string; email?: string; full_name?: string; id?: number; posts_count?: number; // Inferred from name pattern (*_count) } // Resource Array Types export type UserResources = UserResource[]; export type UserSummaryResources = UserSummaryResource[]; export type UserProfileResources = UserProfileResource[]; // Resource Paginated Types export type UserResourcesPaginated = PaginatedResponse<UserResource>; export type UserSummaryResourcesPaginated = PaginatedResponse<UserSummaryResource>; export type UserProfileResourcesPaginated = PaginatedResponse<UserProfileResource>;
PHPDoc Type Hints (Recommended)
For full control over types, use PHPDoc annotations:
/** * @property int $id * @property string $full_name * @property string $email * @property string $avatar_url * @property PostResource[] $posts */ class UserResource extends JsonResource { public function toArray($request) { return [ 'id' => $this->id, 'full_name' => $this->name, 'email' => $this->email, 'avatar_url' => $this->getAvatarUrl(), 'posts' => PostResource::collection($this->posts), ]; } }
How Type Inference Works
The package uses multiple strategies to determine types (in priority order):
- PHPDoc
@property: Highest priority, most reliable - Static Analysis: Detects patterns in
toArray():PostResource::collection(...)→PostResource[]new UserResource(...)→UserResource(bool) $value→boolean->format('Y-m-d')→string
- Model Casts: Inherits types from the underlying Model
- Name Patterns:
*_id,id→number*_at,*_date→stringis_*,has_*,can_*→boolean*_count→number*_url,email,name→string
- Fallback:
unknown(configurable viaunknown_type_fallback)
3. Form Request Interfaces
Generate interfaces from your Form Request validation rules. Perfect for typing your frontend forms.
Laravel Form Requests
// app/Http/Requests/StoreUserRequest.php class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users', 'password' => 'required|string|min:8|confirmed', 'age' => 'nullable|integer|min:18', 'roles' => 'required|array', 'roles.*' => 'exists:roles,id', 'avatar' => 'nullable|image|max:2048', ]; } } // app/Http/Requests/UpdateUserRequest.php class UpdateUserRequest extends FormRequest { public function rules(): array { return [ 'name' => 'sometimes|string|max:255', 'email' => 'sometimes|email', 'bio' => 'nullable|string|max:1000', ]; } }
Generated TypeScript
// Form Request Interfaces export interface StoreUserRequest { age?: number; avatar?: File; email: string; name: string; password: string; roles: any[]; } export interface UpdateUserRequest { bio?: string; email?: string; name?: string; }
Type Mapping from Validation Rules
| Laravel Rule | TypeScript Type |
|---|---|
integer, numeric |
number |
boolean, accepted |
boolean |
array |
any[] |
file, image, mimes |
File |
json |
Record<string, any> |
string, email, url, uuid, date |
string |
required |
Required field (no ?) |
nullable, sometimes |
Optional field (?) |
Conflict Resolution
When you have requests with the same name in different folders, the package automatically adds a prefix:
app/Http/Requests/
├── StoreUserRequest.php → StoreUserRequest
├── UpdateUserRequest.php → UpdateUserRequest
└── Admin/
└── StoreUserRequest.php → AdminStoreUserRequest
└── Api/
└── V1/
└── StoreUserRequest.php → AdminApiV1StoreUserRequest
4. Yup Validation Schemas
Generate Yup schemas from your Form Requests for client-side validation.
Laravel Form Request
class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => 'required|string|min:2|max:255', 'email' => 'required|email', 'password' => 'required|string|min:8|confirmed', 'age' => 'nullable|integer|min:18|max:120', 'website' => 'nullable|url', 'role' => 'required|in:admin,user,guest', ]; } }
Generated Yup Schema
// Yup Validation Schemas // Usage: import * as yup from 'yup'; export const StoreUserRequestSchema = yup.object({ name: yup.string().required('This field is required').min(2, 'Must be at least 2 characters').max(255, 'Must be at most 255 characters'), email: yup.string().required('This field is required').email('Invalid email address'), password: yup.string().required('This field is required').min(8, 'Must be at least 8 characters').oneOf([yup.ref('password_confirmation')], 'Must match confirmation'), age: yup.number().nullable().min(18, 'Must be at least 18').max(120, 'Must be at most 120'), website: yup.string().nullable().url('Invalid URL'), role: yup.string().required('This field is required').oneOf(['admin', 'user', 'guest'], 'Invalid value'), });
Using in React/Vue
// React example with react-hook-form import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { StoreUserRequest, StoreUserRequestSchema } from '@/types/models'; function UserForm() { const { register, handleSubmit, formState: { errors } } = useForm<StoreUserRequest>({ resolver: yupResolver(StoreUserRequestSchema) }); const onSubmit = (data: StoreUserRequest) => { // data is typed! api.post('/users', data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name')} /> {errors.name && <span>{errors.name.message}</span>} {/* ... */} </form> ); }
Supported Yup Validations
| Laravel Rule | Yup Method |
|---|---|
required |
.required() |
nullable |
.nullable() |
email |
.email() |
url |
.url() |
uuid |
.uuid() |
min:n |
.min(n) |
max:n |
.max(n) |
between:a,b |
.min(a).max(b) |
size:n |
.length(n) |
in:a,b,c |
.oneOf(['a','b','c']) |
confirmed |
.oneOf([yup.ref('field_confirmation')]) |
integer |
.integer() |
regex |
.matches() |
5. Zod Validation Schemas
Generate Zod schemas from your Form Requests. Zod is a TypeScript-first schema validation library.
Enable Zod Schemas
TYPESCRIPT_MODELS_GENERATE_ZOD_SCHEMAS=true
Laravel Form Request
class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => 'required|string|min:2|max:255', 'email' => 'required|email', 'role' => 'required|in:admin,user,guest', 'age' => 'nullable|integer|min:18', ]; } }
Generated Zod Schema
// Zod Validation Schemas // Usage: import { z } from 'zod'; export const StoreUserRequestSchema = z.object({ name: z.string().min(2, { message: 'Must be at least 2 characters' }).max(255, { message: 'Must be at most 255 characters' }), email: z.string().email({ message: 'Invalid email address' }), role: z.enum(['admin', 'user', 'guest']), age: z.number().min(18, { message: 'Must be at least 18' }).nullable().optional(), });
Using in React
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { StoreUserRequestSchema } from '@/types/models'; type StoreUserRequest = z.infer<typeof StoreUserRequestSchema>; function UserForm() { const { register, handleSubmit, formState: { errors } } = useForm<StoreUserRequest>({ resolver: zodResolver(StoreUserRequestSchema) }); // ... }
6. Enum Union Literal Types
The package automatically generates TypeScript union literal types from in: validation rules and Rule::in():
Laravel Validation
// Using string rule 'role' => 'required|in:admin,user,guest', // Using Rule::in() 'status' => ['required', Rule::in(['pending', 'approved', 'rejected'])], // Using Enum rule (Laravel 9+) 'type' => ['required', Rule::enum(OrderType::class)],
Generated TypeScript
export interface StoreOrderRequest { role: 'admin' | 'user' | 'guest'; status: 'pending' | 'approved' | 'rejected'; type: 'delivery' | 'pickup' | 'dine_in'; }
This provides much stronger typing than generic string types!
7. Pagination Types
Auto-generated generic pagination interfaces:
export interface PaginationLink { url: string | null; label: string; active: boolean; } export interface PaginatedResponse<T> { data: T[]; current_page: number; first_page_url: string; from: number | null; last_page: number; last_page_url: string; links: PaginationLink[]; next_page_url: string | null; path: string; per_page: number; prev_page_url: string | null; to: number | null; total: number; }
Usage
// Fetch paginated users const response = await api.get<UsersPaginated>('/users'); console.log(response.data); // User[] console.log(response.total); // number console.log(response.current_page); // number
Configuration Options
Models
'include_models' => false, // Disabled by default (use Resources!) 'models_paths' => [ app_path('Models'), ], 'exclude_models' => [ // App\Models\SomeModel::class, ], 'properties_mode' => 'fillable', // fillable, database, or both 'include_accessors' => false, 'include_relations' => true,
Resources
'include_resources' => true, 'resources_paths' => [ app_path('Http/Resources'), ], 'exclude_resources' => [ // App\Http\Resources\SomeResource::class, ], // Type inference to eliminate 'any' types 'infer_resource_types' => true, // What to use when type cannot be inferred 'unknown_type_fallback' => 'unknown', // 'unknown' | 'any' | 'never'
Form Requests
'include_requests' => true, 'requests_paths' => [ app_path('Http/Requests'), ], 'exclude_requests' => [ // App\Http\Requests\SomeRequest::class, ], 'generate_yup_schemas' => true, 'generate_zod_schemas' => false,
Split by Domain
// Split output into multiple files 'split_by_domain' => false, // 'subdirectory' | 'class' | false // How to detect domain from class namespace 'domain_detection' => 'subdirectory', // 'subdirectory' | 'class_basename'
Complete Output Example
// ============================================================================= // Auto-generated TypeScript interfaces from Laravel Models // Generated at: 2024-01-15T10:30:00+00:00 // Do not edit this file manually - it will be overwritten // ============================================================================= // Pagination Interfaces export interface PaginationLink { url: string | null; label: string; active: boolean; } export interface PaginatedResponse<T> { data: T[]; current_page: number; // ... other pagination fields } // Resource Interfaces (with intelligent type inference - no 'any'!) export interface UserResource { id?: number; full_name?: string; avatar_url?: string; posts?: PostResource[]; } export interface UserSummaryResource { id?: number; name?: string; } // Resource Array Types export type UserResources = UserResource[]; export type UserSummaryResources = UserSummaryResource[]; // Resource Paginated Types export type UserResourcesPaginated = PaginatedResponse<UserResource>; export type UserSummaryResourcesPaginated = PaginatedResponse<UserSummaryResource>; // Form Request Interfaces export interface StoreUserRequest { email: string; name: string; password: string; age?: number; } export interface UpdateUserRequest { email?: string; name?: string; } // Yup Validation Schemas // Usage: import * as yup from 'yup'; export const StoreUserRequestSchema = yup.object({ email: yup.string().required('This field is required').email('Invalid email address'), name: yup.string().required('This field is required').max(255, 'Must be at most 255 characters'), password: yup.string().required('This field is required').min(8, 'Must be at least 8 characters'), age: yup.number().nullable().min(18, 'Must be at least 18'), }); export const UpdateUserRequestSchema = yup.object({ email: yup.string().optional().email('Invalid email address'), name: yup.string().optional().max(255, 'Must be at most 255 characters'), });
Security Recommendations
- Never enable in production - Set
TYPESCRIPT_MODELS_ENABLED=false - Use strong tokens - Generate a secure random token
- Restrict IPs - Limit to localhost or known development IPs
- Use HTTPS - Always use HTTPS when transmitting tokens
- Exclude sensitive models - Don't expose internal/admin models
Type Mapping Reference
PHP/Laravel to TypeScript
| PHP/Laravel Type | TypeScript Type |
|---|---|
int, integer |
number |
float, double, decimal |
number |
bool, boolean |
boolean |
array, json, collection |
any[] |
object |
Record<string, any> |
datetime, date, timestamp |
Date |
string (default) |
string |
Validation Rules to TypeScript
| Laravel Rule | TypeScript Type |
|---|---|
integer, numeric |
number |
boolean, accepted |
boolean |
array |
any[] |
file, image |
File |
json |
Record<string, any> |
| Other | string |
License
MIT License - see LICENSE file.