abetwothree/laravel-ts-publish

Create TypeScript declaration types from your PHP models, enums, and other cast classes

Maintainers

Package info

github.com/abetwothree/laravel-ts-publish

pkg:composer/abetwothree/laravel-ts-publish

Fund package maintenance!

abetwothree

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v0.0.7 2026-03-10 13:00 UTC

This package is auto-updated.

Last update: 2026-03-13 23:03:38 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

This is an extremely flexible package that allows you to create TypeScript declaration types from your Laravel PHP models, enums, and other cast classes.

Every application is different, and this package provides the tools to tailor TypeScript types to your specific needs.

Important

Laravel TypeScript Publisher is currently in Beta, functionality, options, and API are subject to change prior to the v1.0.0 release.

Installation

Requires PHP 8.4+ and Laravel 12 or 11

You can install the package via composer:

composer require abetwothree/laravel-ts-publish

You can publish the config file with:

php artisan vendor:publish --tag="ts-publish-config"

Optionally, you can publish the views using

php artisan vendor:publish --tag="laravel-ts-publish-views"

Usage

Publishing Types

You can publish your TypeScript declaration types using the ts:publish Artisan command:

php artisan ts:publish

By default, the generated TypeScript declaration types will be saved to the resources/js/types/ directory and follow default configuration settings.

Additionally, by default, the package will look for models in the app/Models directory and enums in the app/Enums directory. You can customize these settings in the published configuration file.

Preview Mode

You can preview the generated TypeScript output in the console without writing any files by using the --preview flag:

php artisan ts:publish --preview

This is useful for debugging or reviewing what will be generated before committing to file output.

Single-File Republishing

You can republish a single enum or model instead of the entire set by using the --source option with a fully-qualified class name or file path:

php artisan ts:publish --source="App\Enums\Status"
php artisan ts:publish --source="app/Enums/Status.php"

This is significantly faster than a full publish on large projects and is used automatically by the Vite plugin to republish only the file that changed during development.

Automatic Publishing After Migrations

By default, this package will automatically re-publish your TypeScript declaration types after running migrations. This ensures your TypeScript types stay in sync with your database schema changes.

You can disable this behavior in the config file or via environment variable:

// config/ts-publish.php

'run_after_migrate' => false,
TS_PUBLISH_RUN_AFTER_MIGRATE=false

Filtering Models & Enums

You can fully customize which models and enums are included, excluded, or add additional directories to search in. By default, all models in app/Models and all enums in app/Enums are included.

// config/ts-publish.php

// Only publish these specific models (leave empty to include all)
'included_models' => [
    App\Models\User::class,
    App\Models\Post::class,
],

// Exclude specific models from publishing
'excluded_models' => [
    App\Models\Pivot::class,
],

// Search additional directories for models
'additional_model_directories' => [
    'modules/Blog/Models',
],

The same options are available for enums with included_enums, excluded_enums, and additional_enum_directories.

Tip

Include and exclude settings accept both fully-qualified class names and directory paths. When a directory is provided, all matching classes within it will be discovered automatically.

Enums

This package, like these others before it, (spatie/typescript-transformer or modeltyper) can convert enums from PHP to TypeScript for each enum case.

However, PHP enums do not solely consist of enum cases, but can also have methods and static methods that have valuable data to use on the frontend. This package allows you to use these features of PHP enums and publish the return values of these methods in TypeScript as well.

By default, this package will only publish the enum cases and their values to TypeScript, but you can use the provided attributes to specify that you want to call certain methods or static methods and publish their return values in TypeScript as well. See below.

Alternatively, you can enable the auto_include_enum_methods and auto_include_enum_static_methods config options to automatically include all public methods without needing to add attributes. See Auto-Including All Enum Methods for details.

Note

Whether you use the attributes or the global config options, only public methods are ever included. Private and protected methods are always excluded.

Enum Attributes

To use the more advanced transforming features provided by this package for enums you'll need to use the PHP Attributes described below.

All attributes can be found at this link and are under the AbeTwoThree\LaravelTsPublish\Attributes namespace.

List of enum attributes & descriptions:

Attribute Target Description
#[TsEnumMethod] Method Include a method's return values in the TypeScript output. Called per enum case, creates a key/value pair object.
#[TsEnumStaticMethod] Static Method Include a static method's return value in the TypeScript output. Called once, added as a property on the enum.
#[TsEnum] Enum Class Rename the enum when converting to TypeScript. Useful to avoid naming conflicts across namespaces.
#[TsCase] Enum Case Rename, change the frontend value, or add a description to an enum case.

Enum Method #[TsEnumMethod]

Using the TsEnumMethod attribute to specify that the label() method should be called for each enum case value and the return value should be used as the value for the enum case in TypeScript:

use AbeTwoThree\LaravelTsPublish\Attributes\TsEnumMethod;

enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';

    #[TsEnumMethod]
    public function label(): string
    {
        return match($this) {
            self::Active => 'Active User',
            self::Inactive => 'Inactive User',
        };
    }
}

Generated TypeScript declaration type:

export const Status = {
    Active: 'Active User',
    Inactive: 'Inactive User',
    label: {
        Active: 'Active User',
        Inactive: 'Inactive User',
    }
} as const;

The #[TsEnumMethod] attribute accepts optional name and description parameters:

Parameter Type Default Description
name string Method name Customize the key name used in the TypeScript output
description string '' Added as a JSDoc comment above the method output
#[TsEnumMethod(name: 'statusLabel', description: 'Human-readable label for this status')]
public function label(): string
{
    return match($this) {
        self::Active => 'Active User',
        self::Inactive => 'Inactive User',
    };
}

Enum Static Method #[TsEnumStaticMethod]

Using the TsEnumStaticMethod attribute to specify that the options() static method should be called and the return value should be published in TypeScript:

use AbeTwoThree\LaravelTsPublish\Attributes\TsEnumStaticMethod;

enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';

    #[TsEnumStaticMethod]
    public static function options(): array
    {
        return array_map(fn(self $status) => [
            'value' => $status->value,
            'label' => $status->name,
        ], self::cases());
    }
}

Generated TypeScript declaration type:

export const Status = {
    Active: 'active',
    Inactive: 'inactive',
    options: [
        { value: 'active', label: 'Active' },
        { value: 'inactive', label: 'Inactive' },
    ],
} as const;

The #[TsEnumStaticMethod] attribute also accepts the same optional name and description parameters as #[TsEnumMethod]:

#[TsEnumStaticMethod(name: 'allOptions', description: 'Array of all status options')]
public static function options(): array
{
    return array_map(fn(self $status) => [
        'value' => $status->value,
        'label' => $status->name,
    ], self::cases());
}

Enum Class Name #[TsEnum]

Renaming an enum using the TsEnum attribute:

use AbeTwoThree\LaravelTsPublish\Attributes\TsEnum;

#[TsEnum('UserStatus')]
enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';
}

Generated TypeScript declaration type:

export const UserStatus = {
    Active: 'active',
    Inactive: 'inactive',
} as const;

Enum Case Typings #[TsCase]

Renaming an enum case, changing the frontend value, and adding a description using the TsCase attribute:

Parameter Type Default Description
name string Case name Override the case key name in the TypeScript output
value string|int Case value Override the case value in the TypeScript output
description string '' Added as a JSDoc comment above the case
use AbeTwoThree\LaravelTsPublish\Attributes\TsCase;

enum Status: int
{
    #[TsCase(name: 'active_status', value: true, description: 'The user is active')]
    case Active = 1;

    #[TsCase(name: 'inactive_status', value: false, description: 'The user is inactive')]
    case Inactive = 0;
}

Generated TypeScript declaration type:

export const Status = {
    /** The user is active */
    active_status: true,
    /** The user is inactive */
    inactive_status: false,
} as const;

Enum Value & Key Types

As shown above, the enum generated in TypeScript is a JavaScript object with the as const assertion to prevent modification.

However, there are times when you need to validate that a value is a valid enum value or a valid enum case key. For this purpose, this package also generates TypeScript types for the enum values and case keys if the enum is a PHP backed enum.

For every enum, a Type alias is generated from the case values. For backed enums, a Kind alias is also generated from the case names:

Generated Type Source Example
StatusType Case values 'active' | 'inactive'
StatusKind Case names 'Active' | 'Inactive'

Note

The Kind type alias is only generated for backed enums, since unit enums already use case names as their values.

Example:

export const Status = {
    Active: 'active',
    Inactive: 'inactive',
} as const;

export type StatusType = 'active' | 'inactive';

export type StatusKind = 'Active' | 'Inactive'; // Only published if the enum is a backed enum

With those types, you can now validate that a value is a valid enum value or case key:

import { StatusType, StatusKind } from '@js/types/enums';

function setStatus(status: StatusType) {
    // status will only accept 'active' or 'inactive'
}

function setStatusByKey(status: StatusKind) {
    // status will only accept 'Active' or 'Inactive'
}

Enum Metadata & Tolki Enum Package

By default, this package will publish three metadata properties on the enum in TypeScript for the cases, methods, and static methods that are published. These properties are _cases, _methods, and _static.

The purpose for these metadata properties is to be able create an "instance" of the enum from a case value like you'd get on the PHP side. To accomplish this, you need to use the @tolki/enum npm package.

By default, this packages configures the usage of the @tolki/enum package when enums are published.

This is what a published enum looks like when using the @tolki/enum package on the frontend:

import { defineEnum } from '@tolki/enum';

export const Status = defineEnum({
    _cases: ['Active', 'Inactive'],
    _methods: ['label'],
    _static: ['options'],
    Active: 'active',
    Inactive: 'inactive',
    label: {
        Active: 'Active User',
        Inactive: 'Inactive User',
    },
    options: [
        { value: 'active', label: 'Active' },
        { value: 'inactive', label: 'Inactive' },
    ],
} as const);

The defineEnum function from the @tolki/enum package is a factory function that will bind PHP like methods to the enum object.

See more details about defineEnum here.

With the @tolki/enum package, you can now create an "instance" of the enum from a case value like you'd get on the PHP side using the from function:

import { Status } from '@js/types/enums'; // Using example status from the previous example
import { User } from '@js/types/models'; // Assuming you have a User model published as well

const user: User = {
    id: 1,
    name: 'John Doe',
    status: 'active',
}

const userStatus = Status.from(user.status);

// userStatus will now have the following structure:
{
    // cases become just value with the matching value to the model
    value: 'active',
    // methods become just the key/value pair for the matching case
    label: 'Active User',
    // static methods stay as is on the enum
    options: [
        { value: 'active', label: 'Active' },
        { value: 'inactive', label: 'Inactive' },
    ],
}

// Then use the userStatus object in your frontend similarly to how you would use an instance of the enum in PHP:

userStatus.value // 'active'
userStatus.label // 'Active User'
userStatus.options // [
                   //     { value: 'active', label: 'Active' },
                   //     { value: 'inactive', label: 'Inactive' },
                   // ]

The defineEnum function currently also binds a tryFrom & cases functions to the enum.

Enum Metadata Vite Plugin

The @tolki/enum package also provides a Vite plugin that can call the artisan publish command for you and watch for changes to your enums & models to automatically update the generated TypeScript declaration types on the frontend.

For documentation on how to set up the Vite plugin, see this link.

Disabling Enum Metadata or Tolki Enum Package

If you don't plan to use the @tolki/enum package or don't need the metadata properties for your use case, you can disable the generation of these metadata properties in the config file by setting enum_metadata_enabled to false:

// config/ts-publish.php

'enum_metadata_enabled' => false,

If you would like to use the metadata but don't want the @tolki/enum package, you can disable the usage of that package in the config file by setting enums_use_tolki_package to false. This will still generate the metadata properties on the enum, but will not wrap the enum in the defineEnum function from the @tolki/enum package:

// config/ts-publish.php

'enum_metadata_enabled' => true,
'enums_use_tolki_package' => false,

Auto-Including All Enum Methods

By default, only public methods decorated with the #[TsEnumMethod] or #[TsEnumStaticMethod] attributes are included in the TypeScript output. If you'd prefer to include all public methods without needing to add the attribute to every method, you can enable automatic inclusion in your config file:

// config/ts-publish.php

'auto_include_enum_methods' => true,        // Include all public non-static methods
'auto_include_enum_static_methods' => true,  // Include all public static methods

When enabled, every public method declared on the enum will be included in the TypeScript output — you no longer need to add #[TsEnumMethod] or #[TsEnumStaticMethod] to each method. Built-in enum methods like cases(), from(), and tryFrom() are always excluded automatically.

You can still use #[TsEnumMethod] and #[TsEnumStaticMethod] to customize the name or description of individual methods when auto-inclusion is enabled:

enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';

    // Included automatically with defaults (name: 'label', description: '')
    public function label(): string
    {
        return match($this) {
            self::Active => 'Active User',
            self::Inactive => 'Inactive User',
        };
    }

    // Included automatically, but with a custom description from the attribute
    #[TsEnumMethod(description: 'Get the icon name for the status')]
    public function icon(): string
    {
        return match($this) {
            self::Active => 'check',
            self::Inactive => 'x',
        };
    }
}

Caution

These settings are disabled by default for security reasons — enabling them will expose the return values of all public methods on your enums. Make sure you're comfortable with that before enabling them.

Models

This package can also convert your Laravel Eloquent models to TypeScript declaration types. This package will go through your models' properties, mutators, and relations to create TypeScript interfaces that match the structure of your model.

Model Templates & Publishing

By default, this package purposely breaks the model into three separate interfaces for the properties, mutators, and relations to give you more flexibility on which properties you need to use in a concrete situation on your frontend projects. It also generates a fourth interface that extends all three interfaces for when you do want to use all the properties, mutators, and relations together, see below.

If that's still not ideal for your situation, you can change the template used to generate the model types. This package comes with two templates for generating model types:

Template Description
laravel-ts-publish::model-split (Default) Splits into separate interfaces for properties, mutators, and relations
laravel-ts-publish::model-full Combines all properties, mutators, and relations into a single interface

Just change the model_template in the config file to use the template that best fits your needs:

// config/ts-publish.php

'model_template' => 'laravel-ts-publish::model-full',

You are also free to publish the views to modify them or create your own custom template if you want to change the structure of the generated types even more. Just make sure to update the model_template in the config file to point to your new custom template.

Example using the default model-split template with a model that has properties, mutators, and relations

use App\Enums\Status;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * Example database structure for this model:
 * @property int $id
 * @property string $name
 * @property int $is_super_admin
 * @property Status $status
 * @property Post[] $posts
 */
class User extends Model
{
    public function casts(): array
    {
        return [
            'status' => Status::class,
        ];
    }

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    protected function admin(): Attribute
    {
        return Attribute::get(fn(): bool => $this->is_super_admin === 1 ? true : false);
    }
}

Default generated TypeScript declaration type user.ts:

import { StatusType } from '../enums';
import { Profile, Post } from './';

export interface User {
    id: number;
    name: string;
    is_super_admin: number;
    status: StatusType;
}

export interface UserMutators {
    admin: boolean; // From the accessor method
}

export interface UserRelations {
    profile: Profile;
    posts: Post[];
    profile_count: number;
    posts_count: number;
    profile_exists: boolean;
    posts_exists: boolean;
}

export interface UserAll extends User, UserMutators, UserRelations {}

Example Inertia form where we use the entire User interface for the form data, but only need the profile & profile_exists properties from the UserRelations interface for this specific page:

<script setup>
import { useForm } from '@inertiajs/vue3'
import { User, UserRelations } from '@js/types/models';

interface UserForm extends User, Pick<UserRelations, 'profile_exists'>{
    profile: UserRelations['profile'] | null;
}

const { user } = defineProps<{
    user: UserForm;
}>()

const form = useForm<UserForm>({
    ...user,
})

form.profile // Is Profile or null
form.posts // TS error because posts is not part of the UserForm interface
</script>

Example using the model-full template with a model that has all properties in one interface

import { StatusType } from '../enums';
import { Profile, Post } from './';

export interface User {
    // Columns
    id: number;
    name: string;
    is_super_admin: number;
    status: StatusType;
    // Mutators
    admin: boolean; // From the accessor method
    // Relations
    profile: Profile;
    posts: Post[];
    // Counts
    profile_count: number;
    posts_count: number;
    // Exists
    profile_exists: boolean;
    posts_exists: boolean;
}

Same Inertia form example as above would work with this model-full template as well since all properties, mutators, and relations are in the same interface.

You will notice the need to call Omit with more properties to exclude the relation properties that are not needed for this specific page, but that's the tradeoff with using a single interface for the model instead of splitting it into separate interfaces for the properties, mutators, and relations.

<script setup>
import { useForm } from '@inertiajs/vue3'
import { User } from '@js/types/models';

interface UserForm extends Omit<User, 'admin' | 'profile' | 'posts' | 'profile_count' | 'posts_count' | 'posts_exists'> {
    profile: User['profile'] | null;
}

const { user } = defineProps<{
    user: UserForm;
}>()

const form = useForm<UserForm>({
    ...user,
})

form.profile // Is Profile or null
form.posts // TS error because posts is not part of the UserForm interface
</script>

Model Attributes

Like with enums, this package provides a few PHP attributes that you can use to further customize the generated TypeScript declaration types for your models. All attributes can be found at this link and are under the AbeTwoThree\LaravelTsPublish\Attributes namespace.

Attribute Target Description
#[TsCasts] casts() method, $casts property, or model class Specify TypeScript types for model columns. Works similarly to Laravel's casts but for TypeScript.
#[TsType] Custom cast class Specify the TypeScript type for any model property that uses this custom cast class.

Examples using #[TsCasts] attribute

Using #[TsCasts] attribute on casts() method
use AbeTwoThree\LaravelTsPublish\Attributes\TsCasts;

class User extends Model
{
    #[TsCasts([
        'metadata' => '{label: string, value: string}[]',
        'is_super_admin' => 'boolean',
    ])]
    public function casts(): array
    {
        return [
            'status' => Status::class,
            'metadata' => 'array',
            'is_super_admin' => 'number',
        ];
    }
}

Generated TypeScript declaration type:

import { StatusType } from '../enums';

export interface User {
    status: StatusType;
    metadata: {label: string, value: string}[];
    is_super_admin: boolean;
}
Using #[TsCasts] attribute on $casts property & model class name

Similarly, you can use the TsCasts attribute on the $casts property or on the model class itself with the same syntax as above to specify TypeScript types for model properties.

On the $casts property:

use AbeTwoThree\LaravelTsPublish\Attributes\TsCasts;

class User extends Model
{
    #[TsCasts([
        'metadata' => '{label: string, value: string}[]',
        'is_super_admin' => 'boolean',
    ])]
    protected $casts = [
        'status' => Status::class,
        'metadata' => 'array',
        'is_super_admin' => 'number',
    ];
}

On the model class itself:

use AbeTwoThree\LaravelTsPublish\Attributes\TsCasts;

#[TsCasts([
    'metadata' => '{label: string, value: string}[]',
    'is_super_admin' => 'boolean',
])]
class User extends Model
{
    protected $casts = [
        'status' => Status::class,
        'metadata' => 'array',
        'is_super_admin' => 'number',
    ];
}

Tip

It is recommended to place the TsCasts attribute either on the casts() method or the $casts property instead of the model class itself to keep the TypeScript type definitions close to where you are defining the casts for the model properties in PHP. However, the TsCasts attribute can also be used to define the types of mutators and relations, at which point it may make more sense to place the attribute on the model class itself instead of the casts() method or $casts property since those only define types for the model properties.

Custom types using #[TsCasts] attribute

The TsCasts attribute can also receive an array as the value for a property to specify a custom type and where that type should be imported from.

This allows you to define a custom TypeScript type that you can reuse across multiple model properties or across multiple models without having to redefine the type for each property on each model.

use AbeTwoThree\LaravelTsPublish\Attributes\TsCasts;

class User extends Model
{
    #[TsCasts([
        'settings' => 'Record<string, unknown>',
        'metadata' => ['type' => 'MetadataType | null', 'import' => '@js/types/custom'],
        'dimensions' => ['type' => 'ProductDimensions', 'import' => '@js/types/product'],
    ])]
    public function casts(): array
    {
        return [
            'settings' => 'array',
            'metadata' => 'array',
            'dimensions' => 'array',
            'created_at' => 'datetime',
            'updated_at' => 'datetime',
        ];
    }
}

Generated TypeScript declaration type:

import { MetadataType } from '@js/types/custom';
import { ProductDimensions } from '@js/types/product';

export interface User {
    id: number;
    name: string;
    settings: Record<string, unknown>;
    metadata: MetadataType | null;
    dimensions: ProductDimensions;
    created_at: string;
    updated_at: string;
}

Examples using #[TsType] attribute

When you have a custom cast class that you use on one or more model properties, you can use the TsType attribute on that custom cast class to specify what TypeScript type should be used for any model property that uses that custom cast.

Custom cast class with TsType attribute:

use AbeTwoThree\LaravelTsPublish\Attributes\TsType;

#[TsType('{width: number, height: number, depth: number}')]
class ProductDimensionsCast implements CastsAttributes
{    public function get($model, string $key, $value, array $attributes)
    {
        // Custom logic to cast the value to the ProductDimensions type
    }
}

Model using the custom cast class:

class Product extends Model
{
    public function casts(): array
    {
        return [
            'dimensions' => ProductDimensionsCast::class,
        ];
    }
}

Generated TypeScript declaration type:

export interface Product {
    id: number;
    name: string;
    dimensions: {width: number, height: number, depth: number};
}

Using #[TsType] attribute with custom type and import

Similarly to the TsCasts attribute, you can also specify the type and import for a custom cast class using the TsType attribute:

use AbeTwoThree\LaravelTsPublish\Attributes\TsType;

#[TsType(['type' => 'ProductDimensions', 'import' => '@js/types/product'])]
class ProductDimensionsCast implements CastsAttributes
{    public function get($model, string $key, $value, array $attributes)
    {
        // Custom logic to cast the value to the ProductDimensions type
    }
}

Generated TypeScript declaration type:

import { ProductDimensions } from '@js/types/product';

export interface Product {
    id: number;
    name: string;
    dimensions: ProductDimensions;
}

Case Style

This package provides two independent config options to control the casing of generated property and method names:

Config Key Applies To Default
relationship_case Model relationship names, _count, _exists 'snake'
enum_method_case Enum method and static method names 'camel'

Both accept 'snake', 'camel', or 'pascal'.

Relationship Case Style

Controls relationship names in the generated model TypeScript interfaces:

// config/ts-publish.php

'relationship_case' => 'snake', // default
Config Value Relationship hasMany(Post::class) Count Exists
'snake' posts: Post[] posts_count posts_exists
'camel' posts: Post[] postsCount postsExists
'pascal' Posts: Post[] PostsCount PostsExists

Note

For each relationship defined on a model, this package automatically generates _count and _exists properties alongside the relation itself. These correspond to Laravel's withCount and withExists features and are included in every generated model interface.

Enum Method Case Style

Controls the casing of enum method and static method names in the generated TypeScript output:

// config/ts-publish.php

'enum_method_case' => 'camel', // default
Config Value Method getLabel() Static Method AllLabels()
'snake' get_label all_labels
'camel' getLabel allLabels
'pascal' GetLabel AllLabels

Tip

This setting applies to all enum methods — both instance methods (via #[TsEnumMethod] or auto_include_enum_methods) and static methods (via #[TsEnumStaticMethod] or auto_include_enum_static_methods). You can still override individual method names using the name parameter on the attribute.

Timestamps as Date Objects

By default, timestamp columns (date, datetime, timestamp, and their immutable variants) are mapped to string in TypeScript. If your frontend works with Date objects instead, you can enable date mapping:

// config/ts-publish.php

'timestamps_as_date' => true,
Config Value Generated TypeScript Type
false created_at: string
true created_at: Date

Custom TypeScript Type Mappings

This package ships with a comprehensive set of PHP-to-TypeScript type mappings (e.g., integernumber, booleanboolean, jsonobject). You can override existing mappings or add new ones using the custom_ts_mappings config option:

// config/ts-publish.php

'custom_ts_mappings' => [
    'binary' => 'Blob',
    'json' => 'Record<string, unknown>',   // Override the default 'object' mapping
    'money' => 'number',                    // Add a custom type
],

Tip

Custom mappings are merged with the built-in map and take precedence. Type keys are case-insensitive. For per-model type overrides, use the #[TsCasts] attribute instead.

Type-Only Imports

By default, model files use import type { ... } instead of import { ... } for all imported types. This is the correct syntax for stricter TypeScript configurations that enable verbatimModuleSyntax or isolatedModules:

import type { StatusType } from '../enums';
import type { Profile, Post } from './';

export interface User {
    id: number;
    status: StatusType;
}

If your project doesn't require type-only imports, you can disable this with:

// config/ts-publish.php

'use_type_imports' => false,

This only affects model file imports (enum types, model interfaces, and custom #[TsCasts] imports). The enum import { defineEnum } value import from @tolki/enum is unaffected.

Output Options

This package provides several output formats that can be enabled independently:

Config Key Default Description
output_to_files true Write individual .ts files with barrel index.ts exports
output_globals_file false Generate a global.d.ts file with a global TypeScript namespace
output_json_file false Output all generated definitions as a JSON file
output_collected_files_json true Output a JSON list of collected PHP file paths (useful for file watchers)

When output_globals_file is enabled, a global declaration file is created that makes all your types available without explicit imports:

// config/ts-publish.php

'output_globals_file' => true,
'global_filename' => 'laravel-ts-global.d.ts',
'models_namespace' => 'models',
'enums_namespace' => 'enums',

The JSON output from output_collected_files_json is designed to work with build tools and file watchers (like the @tolki/enum Vite plugin) that need to know which PHP source files were collected so they can trigger a re-publish when those files change.

Modular Publishing

By default, this package outputs all generated TypeScript files into flat enums/ and models/ directories:

resources/js/types/
├── enums/
│   ├── article-status.ts
│   ├── invoice-status.ts
│   ├── role.ts
│   └── index.ts
├── models/
│   ├── user.ts
│   ├── invoice.ts
│   ├── shipment.ts
│   └── index.ts
└── global.d.ts

For applications that use a modular architecture (e.g., InterNACHI/modular or a custom module structure), you can enable modular publishing to organize TypeScript output into namespace-derived directory trees that mirror your PHP namespace structure.

Enabling Modular Publishing

Set modular_publishing to true in your config file:

// config/ts-publish.php

'modular_publishing' => true,

With modular publishing enabled, the output structure changes to reflect your PHP namespaces:

resources/js/types/
├── app/
│   ├── enums/
│   │   ├── role.ts
│   │   ├── membership-level.ts
│   │   └── index.ts
│   └── models/
│       ├── user.ts
│       ├── order.ts
│       └── index.ts
├── accounting/
│   ├── enums/
│   │   ├── invoice-status.ts
│   │   └── index.ts
│   └── models/
│       ├── invoice.ts
│       └── index.ts
├── shipping/
│   ├── enums/
│   │   ├── shipment-status.ts
│   │   └── index.ts
│   └── models/
│       ├── shipment.ts
│       └── index.ts
└── global.d.ts

Each namespace directory gets its own barrel index.ts file that exports all types within that directory.

How It Works

Modular publishing converts each class's PHP namespace into a kebab-cased directory path. For example:

PHP Class Output File
App\Models\User app/models/user.ts
App\Enums\Role app/enums/role.ts
Accounting\Models\Invoice accounting/models/invoice.ts
Shipping\Enums\ShipmentStatus shipping/enums/shipment-status.ts
App\Domain\Billing\Models\Invoice app/domain/billing/models/invoice.ts

Import paths between generated files are automatically computed as relative paths based on the namespace directory structure:

// accounting/models/invoice.ts

import { Payment } from '.';                   // Same namespace (accounting/models)
import { User } from '../../app/models';       // Cross-module import
import { InvoiceStatusType } from '../enums';  // Sibling namespace (accounting/enums)

export interface Invoice {
    id: number;
    user_id: number;
    number: string;
    status: InvoiceStatusType;
    subtotal: number;
    tax: number;
    total: number;
    // ...
}

export interface InvoiceRelations {
    user: User;
    payments: Payment[];
    // ...
}

export interface InvoiceAll extends Invoice, InvoiceRelations {}

Stripping a Namespace Prefix

If your modules live under a common namespace prefix (e.g., Modules\), you can strip it from the output path using the namespace_strip_prefix config option:

// config/ts-publish.php

'namespace_strip_prefix' => 'Modules\\',
PHP Class Without Strip Prefix With 'Modules\\' Strip Prefix
Modules\Blog\Models\Article modules/blog/models/article.ts blog/models/article.ts
Modules\Shipping\Enums\Carrier modules/shipping/enums/carrier.ts shipping/enums/carrier.ts

This keeps the output directory structure clean by removing the redundant prefix.

Barrel Files

In modular mode, each namespace directory receives its own barrel index.ts file. For example, accounting/models/index.ts:

export * from './invoice';

And app/models/index.ts:

export * from './address';
export * from './order';
export * from './product';
export * from './user';
// ... all models in this namespace

This allows you to import types from any namespace barrel:

import { User, Order } from '@js/types/app/models';
import { Invoice } from '@js/types/accounting/models';
import { InvoiceStatusType } from '@js/types/accounting/enums';

Extending & Customizing the Pipeline

This package uses a Collector → Generator → Transformer → Writer → Template pipeline. Each stage is fully configurable via the config file, allowing you to extend or replace any component without modifying the package itself:

Pipeline Stage Config Key Default Class Responsibility
Collector model_collector_class ModelsCollector Discovers PHP model/enum classes
Collector enum_collector_class EnumsCollector Discovers PHP enum classes
Generator model_generator_class ModelGenerator Orchestrates transforming and writing
Generator enum_generator_class EnumGenerator Orchestrates transforming and writing
Transformer model_transformer_class ModelTransformer Converts PHP class into TypeScript data
Transformer enum_transformer_class EnumTransformer Converts PHP enum into TypeScript data
Writer model_writer_class ModelWriter Writes TypeScript model files
Writer enum_writer_class EnumWriter Writes TypeScript enum files
Writer barrel_writer_class BarrelWriter Writes barrel index.ts files
Writer globals_writer_class GlobalsWriter Writes global declaration file
Template model_template model-split Blade template for model output
Template enum_template enum Blade template for enum output

To swap a component, create a class that extends the default and override the config key:

// config/ts-publish.php

'model_transformer_class' => App\TypeScript\CustomModelTransformer::class,

Tip

You can also publish and customize the Blade templates directly with php artisan vendor:publish --tag="laravel-ts-publish-views" if you only need to change the output format without modifying the pipeline logic.

Configuration Reference

Below is a quick reference of all available configuration options:

Config Key Type Default Description
run_after_migrate bool true Re-publish types after running migrations
output_to_files bool true Write generated TypeScript to .ts files
output_directory string resources/js/types/ Directory where TypeScript files are written
use_type_imports bool true Use import type instead of import in model files
modular_publishing bool false Organize output into namespace-derived directory trees
namespace_strip_prefix string '' Strip this prefix from namespaces in modular mode
relationship_case string 'snake' Case style for relationships: snake, camel, or pascal
enum_method_case string 'camel' Case style for enum methods: snake, camel, or pascal
timestamps_as_date bool false Map date/datetime/timestamp to Date instead of string
custom_ts_mappings array [] Override or extend PHP-to-TypeScript type mappings
auto_include_enum_methods bool false Include all public non-static enum methods without attributes
auto_include_enum_static_methods bool false Include all public static enum methods without attributes
enum_metadata_enabled bool true Include _cases, _methods, _static metadata on enums
enums_use_tolki_package bool true Wrap enums in defineEnum() from @tolki/enum
output_globals_file bool false Generate a global.d.ts namespace file
global_directory string resources/js/types/ Directory for the global declaration file
global_filename string laravel-ts-global.d.ts Filename for the global declaration file
models_namespace string 'models' Namespace label used in the global declaration file
enums_namespace string 'enums' Namespace label used in the global declaration file
output_json_file bool false Output all definitions as a JSON file
json_filename string laravel-ts-definitions.json Filename for the JSON output
json_output_directory string resources/js/types/ Directory for the JSON output
output_collected_files_json bool true Output collected PHP file paths as JSON (for file watchers)
collected_files_json_filename string laravel-ts-collected-files.json Filename for the collected files JSON
collected_files_json_output_directory string resources/js/types/ Directory for the collected files JSON
model_template string laravel-ts-publish::model-split Blade template for model TypeScript output
enum_template string laravel-ts-publish::enum Blade template for enum TypeScript output
globals_template string laravel-ts-publish::globals Blade template for global declaration output
included_models array [] Only publish these models (empty = all)
excluded_models array [] Exclude these models from publishing
additional_model_directories array [] Extra directories to search for models
included_enums array [] Only publish these enums (empty = all)
excluded_enums array [] Exclude these enums from publishing
additional_enum_directories array [] Extra directories to search for enums

See the full configuration file for detailed comments on each option.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.