oi-lab/oi-laravel-ts
Generates TypeScript interfaces from Laravel Eloquent models, including relationships, casts, PHPDoc types, and DataObjects. Supports watch mode, namespace filtering, UUID/ULID key typing, and optional JSON-LD output.
A Laravel package that automatically generates TypeScript interfaces from your Eloquent models, complete with relationships, custom casts, and DataObjects support.
string, integer keys as numberThis package uses a modular architecture with clear separation of concerns, organized in two main pipelines:
For detailed architecture documentation, see ARCHITECTURE.md.
composer require oi-lab/oi-laravel-ts
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
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,
// 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' => [],
// 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',
],
],
];
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.
Automatically regenerate when models change:
php artisan oi:gen-ts --watch
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;
}
Add properties that aren't in your database schema:
// config/oi-laravel-ts.php
'custom_props' => [
'User' => [
'full_name' => 'string',
'avatar_url' => 'string',
],
],
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;
}
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 bool $isActive = true,
) {}
public static function fromModel(Knowledge $knowledge): self { /* ... */ }
}
export interface IKnowledgeData {
id: string;
state: 'draft' | 'published' | 'archived';
source?: IKnowledgeSourceData;
tags?: IKnowledgeTagData[];
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[].
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.
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;
}
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;
// ...
}
Reference external TypeScript types:
'custom_props' => [
'User' => [
'settings' => '@/types/settings|UserSettings',
],
],
Generates:
import { UserSettings } from '@/types/settings';
export interface IUser {
settings: UserSettings;
}
php artisan oi:gen-ts
import { IUser, IPost } from '@/types/interfaces';
const user: IUser = await fetchUser();
const posts: IPost[] = user.posts || [];
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);
}
This package includes comprehensive test coverage with 142 tests and 344 assertions.
# 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
For detailed testing documentation, see TESTING.md.
Contributions are welcome! Please feel free to submit a Pull Request.
When contributing:
vendor/bin/pestThis package is open-source software licensed under the MIT license.
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:
For support, please open an issue on the GitHub repository.
How can I help you explore Laravel packages today?