oi-lab/oi-laravel-ts
OI Laravel TypeScript Generator
A Laravel package that automatically generates TypeScript interfaces from your Eloquent models, complete with relationships, custom casts, and DataObjects support.
Features
- Automatic Interface Generation: Converts Eloquent models to TypeScript interfaces
- Relationship Support: Handles all Laravel relationship types (HasOne, HasMany, BelongsTo, etc.)
- Custom Casts: Supports Laravel custom casts and automatically detects DataObjects
- DTO Support: Emits interfaces for spatie/laravel-data style DTOs (no dependency required), with enum literal unions and nested DTOs
- PHPDoc Support: Reads PHPDoc annotations for complex types
- Watch Mode: Monitor your models directory and regenerate on changes
- Namespace Filters: Exclude third-party models entirely, or promote them to extension interfaces
- Smart Primary Key Types: UUID / ULID keys are typed as
string, integer keys asnumber - Configurable: Extensive configuration options for customization
- JSON-LD Support: Optional support for JSON-LD data structures
Architecture
This package uses a modular architecture with clear separation of concerns, organized in two main pipelines:
Pipeline 1: Eloquent Analysis
- Eloquent: Facade for model analysis and schema generation
- ModelDiscovery: Discovers all Eloquent models in the application
- TypeExtractor: Extracts type information from models
- CastTypeResolver: Resolves custom Laravel casts to TypeScript types
- RelationshipResolver: Detects and extracts relationship metadata
- DataObjectAnalyzer: Analyzes PHP DataObject classes
- PhpToTypeScriptConverter: Converts PHP types to TypeScript
- SchemaBuilder: Orchestrates schema building for all models
Pipeline 2: TypeScript Generation
- Convert: Main orchestrator coordinating the conversion process
- TypeScriptTypeConverter: Handles schema to TypeScript type conversion
- DataObjectProcessor: Processes PHP DataObjects and generates their interfaces
- DataClassResolver / DataClassAnalyzer / DataClassProcessor: Discover, analyze and emit spatie/laravel-data style DTOs
- ModelInterfaceGenerator: Generates TypeScript interfaces for Laravel models
- ImportManager: Manages TypeScript import statements
- JsonLdGenerator: Generates JSON-LD support interfaces
For detailed architecture documentation, see ARCHITECTURE.md.
Requirements
- PHP 8.2+
- Laravel 11.0+, 12.0+, or 13.0+
Installation
composer require oi-lab/oi-laravel-ts
Local Development
For local development, add this to your main project's composer.json:
{
"repositories": [
{
"type": "path",
"url": "./packages/oi-lab/oi-laravel-ts"
}
]
}
Then:
composer require oi-lab/oi-laravel-ts
Configuration
Publish the configuration file:
php artisan vendor:publish --tag=oi-laravel-ts-config
This creates config/oi-laravel-ts.php with the following options:
return [
// Output path for generated TypeScript file
'output_path' => resource_path('js/types/interfaces.ts'),
// Include _count fields for relationships
'with_counts' => true,
// Enable JSON-LD support
'with_json_ld' => false,
// Follow relationships to generate interfaces for referenced models
// (incl. models attached through traits, e.g. spatie/laravel-permission)
'discover_related_models' => true,
// Add every Eloquent model of these namespaces to the schema, as if in app/Models
'included_model_namespaces' => [],
// Exclude models in these namespaces entirely (incl. relation fields pointing to them)
'excluded_namespaces' => [],
// Models in these namespaces emit I{Name}Extended extends I{Name} interfaces
'extended_namespaces' => [],
// Save intermediate schema.json for debugging
'save_schema' => false,
// Namespaces holding spatie/laravel-data style DTOs to emit as I{ClassName}
'data_namespaces' => [],
// Give a DTO a distinct interface name when two share a short class name
'data_aliases' => [],
// 'null': `?` means "key may be absent", `| null` means "value may be null"
// 'optional': legacy — nullable or defaulted properties all render as `?`
'data_nullable_style' => 'null',
// When true, a model mapped to a DTO no longer emits its own Eloquent interface
'data_replaces_model' => false,
// Explicit model => DTO map (otherwise inferred from the DTO's fromModel() factory)
'data_for_model' => [],
// Define specific types for model properties
'props_with_types' => [],
// Add custom properties to models
'custom_props' => [
'Organization' => [
'uuid' => 'string',
],
],
];
Usage
Basic Generation
Generate TypeScript interfaces from your models:
php artisan oi:gen-ts
This will scan all models in app/Models and generate a TypeScript file at the configured output path.
Watch Mode
Automatically regenerate when models change:
php artisan oi:gen-ts --watch
Example Output
Given a Laravel model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = ['name', 'email', 'bio'];
protected $casts = [
'email_verified_at' => 'datetime',
];
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}
The package generates:
export interface IUser {
id: number;
name: string;
email: string;
bio: string;
email_verified_at?: string;
created_at: string;
updated_at: string;
posts?: IPost[];
posts_count?: number;
}
Advanced Features
Custom Properties
Add properties that aren't in your database schema:
// config/oi-laravel-ts.php
'custom_props' => [
'User' => [
'full_name' => 'string',
'avatar_url' => 'string',
],
],
DataObject Support
The package automatically detects and converts custom DataObjects:
// Your model
class Page extends Model
{
protected $casts = [
'metadata' => MetadataCast::class,
];
}
// Your Cast
class MetadataCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): Metadata
{
return Metadata::fromArray(json_decode($value, true));
}
}
// Generated TypeScript
export interface IMetadata {
title: string;
description?: string;
}
export interface IPage {
id: number;
metadata?: IMetadata | null;
}
DTO Support (spatie/laravel-data)
Register the namespaces holding your Data Transfer Objects and every DTO is
emitted as an I{ClassName} interface. Detection is structural — no dependency
on spatie/laravel-data is required:
'data_namespaces' => [
'App\\Data',
],
// App\Data\Knowledge\KnowledgeData
class KnowledgeData extends \Spatie\LaravelData\Data
{
public function __construct(
public readonly string $id,
public readonly KnowledgeState $state, // backed enum
public readonly ?KnowledgeSourceData $source, // nested DTO
/** @var KnowledgeTagData[]|null */
public readonly ?array $tags,
public readonly string|Optional $slug, // may be absent from the payload
public readonly bool $isActive = true,
) {}
public static function fromModel(Knowledge $knowledge): self { /* ... */ }
}
export interface IKnowledgeData {
id: string;
state: 'draft' | 'published' | 'archived';
source: IKnowledgeSourceData | null;
tags: IKnowledgeTagData[] | null;
slug?: string;
isActive: boolean;
}
Property names are kept verbatim (camelCase), backed enums become literal unions,
nested DTOs become I{Name}, and typed arrays declared via a property
@var Foo[] annotation become IFoo[].
The interface describes the JSON the DTO produces, so ? and | null mean
different things: ? marks a key that may be absent (an Optional / Lazy
property), | null a key that is always present but may hold null. A default
value makes nothing optional. See
data_nullable_style to fall back to the legacy
rendering.
By default DTO interfaces coexist with the Eloquent model interfaces. To make a
DTO the single source of truth for its model — suppressing the model's own
I{Model} interface — enable data_replaces_model. The model is inferred from
the DTO's fromModel() factory, or set explicitly via data_for_model:
'data_replaces_model' => true,
'data_for_model' => [
App\Models\Knowledge::class => App\Data\Knowledge\KnowledgeData::class,
],
Note: with
data_replaces_modelenabled, a relationship on another model that points to a replaced model will reference an interface that is no longer generated.
Namespace Filters
Exclude third-party or package models from the schema entirely:
'excluded_namespaces' => [
'OiLab\\Prestashop\\Models',
],
Models in these namespaces are dropped — including when reached through a relationship — and any relationship field pointing to them is stripped from other interfaces.
Alternatively, turn package model variants into extension interfaces:
'extended_namespaces' => [
'OiLab\\Prestashop\\Extended\\Models',
],
For each extended model whose short name matches a base model in the schema, the generator emits:
export interface IUserExtended extends IUser {
prestashop_id: number;
}
UUID / ULID Primary Keys
Primary key types are resolved automatically. Models using HasUuids, HasUlids, or declaring $keyType = 'string' generate id: string instead of id: number:
// Standard auto-increment model
export interface IPost {
id: number;
// ...
}
// UUID model (uses HasUuids trait or $keyType = 'string')
export interface IOrder {
id: string;
// ...
}
Import External Types
Reference external TypeScript types:
'custom_props' => [
'User' => [
'settings' => '@/types/settings|UserSettings',
],
],
Generates:
import { UserSettings } from '@/types/settings';
export interface IUser {
settings: UserSettings;
}
Examples
Complete Workflow
- Define your models with relationships and casts
- Configure custom properties if needed
- Run the generator:
php artisan oi:gen-ts
- Use the generated interfaces in your TypeScript code:
import { IUser, IPost } from '@/types/interfaces';
const user: IUser = await fetchUser();
const posts: IPost[] = user.posts || [];
Integration with Inertia.js
import { PageProps } from '@inertiajs/core';
import { IUser } from '@/types/interfaces';
interface Props extends PageProps {
user: IUser;
}
export default function Dashboard({ user }: Props) {
// TypeScript knows all User properties
console.log(user.email);
}
Testing
This package includes comprehensive test coverage with 142 tests and 344 assertions.
Run Tests
# Run all tests
vendor/bin/pest
# Run specific test suite
vendor/bin/pest tests/Unit
vendor/bin/pest tests/Feature
# Run with coverage
vendor/bin/pest --coverage
Test Coverage
- ✅ Type conversion (PHP → TypeScript)
- ✅ Model analysis and schema building
- ✅ Relationship detection (HasMany, BelongsTo, etc.)
- ✅ Custom cast resolution
- ✅ DataObject handling
- ✅ DTO (spatie/laravel-data) generation, enums and nested DTOs
- ✅ TypeScript interface generation
- ✅ Namespace exclusion and extension interfaces
- ✅ UUID / ULID primary key type resolution
- ✅ Integration tests for full pipeline
For detailed testing documentation, see TESTING.md.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
When contributing:
- Write tests for new features
- Ensure all tests pass:
vendor/bin/pest - Follow existing code style
- Update documentation as needed
License
This package is open-source software licensed under the MIT license.
Credits
Olivier Lacombe - Creator and maintainer
Olivier is a Product & Technology Director based in Montpellier, France, with over 20 years of experience innovating in UX/UI and emerging technologies. He specializes in guiding enterprises toward cutting-edge digital solutions, combining user-centered design with continuous optimization and artificial intelligence integration.
Projects & Resources:
- OI Dev Docs - Documentation for all Open Source OI Lab packages
- OnAI - Training courses and masterclasses on generative AI for businesses
- Promptr - Prompt engineering Management Platform
Support
For support, please open an issue on the GitHub repository.
Related Packages
Generate TypeScript types from Eloquent models, Enums & FormRequests.
Generate Typescript definitions for your Eloquent models
Automatically generate TypeScript types and validation schemas from Laravel Mode...
PHP package for Laravel to type Eloquent models, routes, Spatie Settings with au...
This package gives Eloquent models the ability to manage their friendships.