PHPackages                             abetwothree/laravel-ts-publish - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. abetwothree/laravel-ts-publish

ActiveLibrary[Database &amp; ORM](/categories/database)

abetwothree/laravel-ts-publish
==============================

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

v1.5.0(1mo ago)5942[2 PRs](https://github.com/abetwothree/laravel-ts-publish/pulls)MITPHPPHP ^8.4CI passing

Since Mar 8Pushed 3d agoCompare

[ Source](https://github.com/abetwothree/laravel-ts-publish)[ Packagist](https://packagist.org/packages/abetwothree/laravel-ts-publish)[ Docs](https://github.com/abetwothree/laravel-ts-publish)[ GitHub Sponsors](https://github.com/abetwothree)[ RSS](/packages/abetwothree-laravel-ts-publish/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (47)Versions (42)Used By (0)

Laravel TypeScript Enums, Models, &amp; Resources Publisher
===========================================================

[](#laravel-typescript-enums-models--resources-publisher)

[![Latest Version on Packagist](https://camo.githubusercontent.com/facd1a63a8edc7938ab9510b7d3afcec1a965139f89c11e03b9d67964eec7051/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61626574776f74687265652f6c61726176656c2d74732d7075626c6973682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/abetwothree/laravel-ts-publish)[![Laravel Compatibility](https://camo.githubusercontent.com/d24e9ed4f248e3269102e2fe02f0ff93d2ba0dfa682fec360654ceece946591a/68747470733a2f2f62616467652e6c61726176656c2e636c6f75642f62616467652f61626574776f74687265652f6c61726176656c2d74732d7075626c697368)](https://packagist.org/packages/abetwothree/laravel-ts-publish)[![GitHub Tests Action Status](https://camo.githubusercontent.com/586aab25984053131997ad6be9d3e1341a35d99c929a833f6c4ac1bc4ff70f30/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f61626574776f74687265652f6c61726176656c2d74732d7075626c6973682f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/abetwothree/laravel-ts-publish/actions?query=workflow%3Arun-tests+branch%3Amain)[![Coverage](assets/coverage.svg)](https://github.com/abetwothree/laravel-ts-publish/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/0941e02c588c5ddb88f96ab092a9adf3c6b95f2b29029c0e1250a1779e4ad1c5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f61626574776f74687265652f6c61726176656c2d74732d7075626c6973682f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/abetwothree/laravel-ts-publish/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/1a73e2cc5b27e53cf67d820e719c39c2d5cba8aff2485be7711ddcaf90f85356/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61626574776f74687265652f6c61726176656c2d74732d7075626c6973682e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/abetwothree/laravel-ts-publish)

[![Laravel TypeScript Publisher Logo](./assets/laravel-typescript-publish-logo-short.svg)](./assets/laravel-typescript-publish-logo-short.svg)

This is an extremely flexible package that allows you to transform Laravel PHP models, enums, API resources, and other cast classes into TypeScript declaration types.

Enums are treated as functional objects with support for PHP-like enum functions and the inclusion of your custom methods in your enums.

Every Laravel application is different, and this package aims to provide the tools to tailor TypeScript types to your specific needs while providing powerful backend &amp; frontend tooling to keep your frontend types in sync with your backend PHP code.

For examples of the generated TypeScript output, see [these output examples](workbench/resources/js/types/).

Table of Contents
-----------------

[](#table-of-contents)

- 📦 [Installation](#installation)
- 🚀 [Usage](#usage)
- 🏷️ [Enums](#enums)
- 🗃️ [Models](#models)
- 📡 [API Resources](#api-resources)
- 🧬 [Extending Interfaces](#extending-interfaces-with-tsextends--configs)
- ❌ [Excluding Content](#excluding-with-tsexclude)
- 🔤 [Casing Configurations](#casing-configurations)
- 🌐 [Enum API Resource](#json-enum-http-api-resource)
- 📂 [Modular Publishing](#modular-publishing)
- 🔧 [Customizing the Pipeline](#extending--customizing-the-pipeline)
- ⚡ [Pre-Command Hook](#pre-command-hook)
- ⚙️ [Configuration Reference](#configuration-reference)

Installation
------------

[](#installation)

**Requires PHP 8.4+ and supports Laravel 13, 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
-----

[](#usage)

### Publishing Types

[](#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/data/` directory and follow default configuration settings.

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

For a full installation and setup guide, see the [Installation &amp; Setup](WORKFLOW.md) documentation.

#### Preview Mode

[](#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

[](#single-file-republishing)

You can republish a single enum, model, or resource 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"
php artisan ts:publish --source="App\Http\Resources\UserResource"
```

This is significantly faster than a full publish on large projects and is used automatically by the [Vite plugin](#enum-metadata-vite-plugin) to republish only the file that changed during development.

#### Automatic Publishing After Migrations

[](#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 &amp; Resources

[](#filtering-models-enums--resources)

You can fully customize which models, enums, and resources are included or excluded, and add additional directories to search in. By default, all models in `app/Models`, all enums in `app/Enums`, and all resources in `app/Http/Resources` 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`, and for resources with `included_resources`, `excluded_resources`, and `additional_resource_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.

#### Conditional Publishing

[](#conditional-publishing)

You can choose to publish only enums, only models, or only resources, either through configuration or command flags.

##### Via Configuration

[](#via-configuration)

Disable enum, model, or resource publishing entirely in the config file:

```
// config/ts-publish.php

'publish_enums' => true,
'publish_models' => true,
'publish_resources' => true,
```

Setting any to `false` will skip that type on every run, including automatic post-migration publishing.

##### Via Command Flags

[](#via-command-flags)

Use the `--only-enums`, `--only-models`, or `--only-resources` flags to limit a single run:

```
php artisan ts:publish --only-enums
php artisan ts:publish --only-models
php artisan ts:publish --only-resources
```

These flags cannot be combined — passing any two together will return an error.

##### Config &amp; Flag Conflicts

[](#config--flag-conflicts)

When a command flag requests a type that is disabled in config (e.g. `--only-enums` while `publish_enums` is `false`), the command will prompt you to confirm whether to override the config setting. In non-interactive environments (CI, queued jobs, post-migration hooks), the config value is respected and the command exits gracefully.

If all types end up disabled (all config values are `false` and no override flag is given), the command prints a warning and exits with a success status.

#### Verbosity Levels

[](#verbosity-levels)

The `ts:publish` command supports three verbosity levels using the standard Artisan verbosity flags:

FlagOutput`--quiet` / `-q`No output at all — only the exit code indicates success or failure. Ideal for automated tooling like the [Vite plugin](#enum-metadata-vite-plugin).*(default)*A compact summary showing the output directory, file counts, and any extra files generated (barrels, globals, JSON).`--verbose` / `-v`Detailed tables listing every generated file with per-file metadata (cases, methods, columns, mutators, relations).```
# Compact summary (default)
php artisan ts:publish

# Detailed tables
php artisan ts:publish -v

# Silent — for scripts, CI, or the Vite plugin
php artisan ts:publish --quiet
```

In quiet mode, files are still generated normally — only console output is suppressed. The [Vite plugin](#enum-metadata-vite-plugin) passes `--quiet` by default since it only needs the exit code.

Enums
-----

[](#enums)

This package, like others before it ([spatie/typescript-transformer](https://github.com/spatie/typescript-transformer) and [modeltyper](https://github.com/fumeapp/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](#auto-including-all-enum-methods) for details.

Tip

This package also provides an `EnumResource` JSON resource that lets you return a flattened, instance-specific representation of any enum case from your API routes. See [JSON Enum HTTP API Resource](#json-enum-http-api-resource) 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

[](#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](https://github.com/abetwothree/laravel-ts-publish/tree/main/src/Attributes) and are under the `AbeTwoThree\LaravelTsPublish\Attributes` namespace.

List of enum attributes &amp; descriptions:

AttributeTargetDescription`#[TsEnumMethod]`MethodInclude a method's return values in the TypeScript output. Called per enum case, creates a key/value pair object.`#[TsEnumStaticMethod]`Static MethodInclude a static method's return value in the TypeScript output. Called once, added as a property on the enum.`#[TsEnum]`Enum ClassRename the enum or add a description when converting to TypeScript. Useful to avoid naming conflicts across namespaces.`#[TsCase]`Enum CaseRename, change the frontend value, or add a description to an enum case.`#[TsExclude]`Class, MethodExclude an entire enum or specific enum methods from the TypeScript output. See [Excluding with TsExclude](#excluding-with-tsexclude).### Enum Method #\[TsEnumMethod\]

[](#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`, `description`, and `params` parameters:

ParameterTypeDefaultDescription`name``string`Method nameCustomize the key name used in the TypeScript output`description``string``''`Added as a JSDoc comment above the method output`params``array``[]`Named arguments to pass when invoking the method (see example below)```
#[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',
    };
}
```

#### Methods with Required Parameters

[](#methods-with-required-parameters)

Methods that require parameters are **skipped by default** — they will not appear in the generated TypeScript output. This prevents producing misleading `null` values for methods that can't be called without arguments.

To include a method that requires parameters, use the `params` property on the attribute to provide named arguments:

```
enum Priority: int
{
    case Low = 0;
    case Medium = 1;
    case High = 2;

    #[TsEnumMethod(description: 'Compare with threshold', params: ['threshold' => 1])]
    public function isAboveThreshold(int $threshold): bool
    {
        return $this->value > $threshold;
    }
}
```

Generated TypeScript:

```
export const Priority = {
    Low: 0,
    Medium: 1,
    High: 2,
    /** Compare with threshold */
    isAboveThreshold: {
        Low: false,
        Medium: false,
        High: true,
    },
} as const;
```

The `params` values must be constant expressions (scalars, arrays of scalars) since they are defined inside a PHP attribute. The values are spread as named arguments when the method is invoked for each enum case.

Note

Methods with only optional parameters (parameters with default values) are still included without needing to set `params`, since they can be called without arguments.

### Enum Static Method #\[TsEnumStaticMethod\]

[](#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 accepts the same optional `name`, `description`, and `params` parameters as `#[TsEnumMethod]`:

ParameterTypeDefaultDescription`name``string`Method nameCustomize the key name used in the TypeScript output`description``string``''`Added as a JSDoc comment above the method output`params``array``[]`Named arguments to pass when invoking the method```
#[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());
}
```

Like `#[TsEnumMethod]`, static methods with required parameters are **skipped by default** unless `params` is provided:

```
#[TsEnumStaticMethod(description: 'Filter by minimum priority', params: ['minimum' => 1])]
public static function filterByMinimum(int $minimum): array
{
    return array_filter(self::cases(), fn (self $case) => $case->value >= $minimum);
}
```

### Enum Class Name #\[TsEnum\]

[](#enum-class-name-tsenum)

Renaming an enum or adding a description using the `TsEnum` attribute:

ParameterTypeDefaultDescription`name``string`Enum class nameOverride the TypeScript const name`description``string``''`Added as a JSDoc comment above the enum. Takes priority over any PHPDoc description.```
use AbeTwoThree\LaravelTsPublish\Attributes\TsEnum;

#[TsEnum('UserStatus', description: 'All possible user account statuses')]
enum Status: string
{
    case Active = 'active';
    case Inactive = 'inactive';
}
```

Generated TypeScript declaration type:

```
/** All possible user account statuses */
export const UserStatus = {
    Active: 'active',
    Inactive: 'inactive',
} as const;
```

### Enum Case Typings #\[TsCase\]

[](#enum-case-typings-tscase)

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

ParameterTypeDefaultDescription`name``string`Case nameOverride the case key name in the TypeScript output`value``string|int`Case valueOverride 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 &amp; Key Types

[](#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](https://www.php.net/manual/en/language.enumerations.backed.php).

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 TypeSourceExample`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/data/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 &amp; Tolki Enum Package

[](#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 of these metadata properties is to be able to 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](https://tolki.abe.dev/enums/) npm package.

By default, this package 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](https://tolki.abe.dev/enums/enum-utilities-list.html#defineenum).

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/data/enums'; // Using example status from the previous example
import { User } from '@js/types/data/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 the `tryFrom` and `cases` functions to the enum.

### Enum Metadata Vite Plugin

[](#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 &amp; 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](https://tolki.abe.dev/enums/enum-vite-plugin.html).

### Disabling Enum Metadata or Tolki Enum Package

[](#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

[](#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.

Note

Methods with required parameters are automatically skipped in auto-include mode since there is no attribute to provide `params` on. To include a method that requires parameters, add the `#[TsEnumMethod]` or `#[TsEnumStaticMethod]` attribute with the `params` property set.

You can still use `#[TsEnumMethod]` and `#[TsEnumStaticMethod]` to customize the `name`, `description`, or `params` 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.

### PHPDoc Descriptions for Enums

[](#phpdoc-descriptions-for-enums)

This package automatically reads PHPDoc doc blocks and outputs them as JSDoc comments in the generated TypeScript. Descriptions are read from the following locations:

LocationSourceJSDoc PlacementEnum classDoc block above the enum classAbove the `export const` declarationEnum casesDoc block above each caseAbove the case propertyInstance methodsDoc block above the methodAbove the method propertyStatic methodsDoc block above the static methodAbove the static method propertyLines starting with `@` (such as `@param`, `@return`, `@phpstan-type`, etc.) are automatically filtered out — only the human-readable description text is included.

**Priority:** If an element has both a PHPDoc doc block and an attribute with a `description` parameter (e.g., `#[TsEnum(description: ...)]`, `#[TsCase(description: ...)]`, `#[TsEnumMethod(description: ...)]`), the **attribute description always takes priority** over the doc block.

```
/**
 * Represents the priority level of a task.
 *
 * @phpstan-type PriorityValue = int
 */
enum Priority: int
{
    /** Lowest priority level */
    case Low = 0;

    /** Standard priority */
    case Medium = 1;

    /** Highest priority level */
    case High = 2;

    /** Human-readable label for the priority */
    #[TsEnumMethod]
    public function label(): string
    {
        return match($this) {
            self::Low => 'Low Priority',
            self::Medium => 'Medium Priority',
            self::High => 'High Priority',
        };
    }
}
```

Generated TypeScript:

```
/** Represents the priority level of a task. */
export const Priority = {
    /** Lowest priority level */
    Low: 0,
    /** Standard priority */
    Medium: 1,
    /** Highest priority level */
    High: 2,
    /** Human-readable label for the priority */
    label: {
        Low: 'Low Priority',
        Medium: 'Medium Priority',
        High: 'High Priority',
    },
} as const;
```

Models
------

[](#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 &amp; Publishing

[](#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.

Note

Any mutator added to the `$appends` property on the model will be included in the model properties interface in the split template since those are always included when the model is serialized to JSON.

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:

TemplateDescription`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 interfaceJust 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

[](#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 | null;
    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` &amp; `profile_exists` properties from the `UserRelations` interface for this specific page:

```

import { useForm } from '@inertiajs/vue3'
import { User, UserRelations } from '@js/types/data/models';

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

const { user } = defineProps()

const form = useForm({
    ...user,
})

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

```

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

[](#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 | null;
    posts: Post[];
    // Counts
    profile_count: number;
    posts_count: number;
    // Exists
    profile_exists: boolean;
    posts_exists: boolean;
}
```

The 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.

```

import { useForm } from '@inertiajs/vue3'
import { User } from '@js/types/data/models';

interface UserForm extends Omit {
    profile: User['profile'] | null;
}

const { user } = defineProps()

const form = useForm({
    ...user,
})

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

```

### Nullable Relations

[](#nullable-relations)

By default, this package detects whether singular relations should be typed as nullable (`| null`) based on the relation type and database schema:

Relation TypeStrategyBehavior`HasOne``nullable`Always add `null` — the related record may not exist`MorphOne``nullable`Always add `null``HasOneThrough``nullable`Always add `null``BelongsTo``fk`Add `null` only when the foreign key column is nullable in the database`MorphTo``morph`Add `null` when either the morph type or morph id column is nullable`HasMany``never`Never nullable (returns an empty array, not null)`BelongsToMany``never`Never nullable`MorphMany``never`Never nullable`MorphToMany``never`Never nullableFor example, a `User` model with a `HasOne` profile and a `HasMany` posts relation generates:

```
export interface UserRelations {
    profile: Profile | null;  // HasOne — always nullable
    posts: Post[];            // HasMany — never nullable
}
```

A `Post` model with a non-nullable `user_id` FK and a nullable `category_id` FK generates:

```
export interface PostRelations {
    author: User;              // BelongsTo — user_id is NOT NULL
    category_rel: Category | null; // BelongsTo — category_id is nullable
}
```

#### Disabling Nullable Relations

[](#disabling-nullable-relations)

To disable nullable relation detection entirely and keep all singular relations non-nullable:

```
// config/ts-publish.php

'nullable_relations' => false,
```

#### Overriding the Nullability Strategy

[](#overriding-the-nullability-strategy)

You can override the default strategy for any relation type using the `relation_nullability_map` config. Keys are fully qualified class names — use the `::class` syntax for safety and IDE autocompletion:

```
// config/ts-publish.php

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;

'relation_nullability_map' => [
    BelongsTo::class => 'nullable',  // Make all BelongsTo always nullable
    HasOne::class    => 'never',     // Make HasOne never nullable
],
```

This also supports custom relation types from third-party packages:

```
use SomePackage\Relations\BelongsToTenant;

'relation_nullability_map' => [
    BelongsToTenant::class => 'fk',
],
```

Available strategies: `'nullable'` (always), `'never'` (never), `'fk'` (check FK column), `'morph'` (check morph columns).

See `AbeTwoThree\LaravelTsPublish\RelationMap` for the full default map.

### Model Attributes

[](#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](https://github.com/abetwothree/laravel-ts-publish/tree/main/src/Attributes) and are under the `AbeTwoThree\LaravelTsPublish\Attributes` namespace.

AttributeTargetDescription`#[TsCasts]``casts()` method, `$casts` property, or model classSpecify TypeScript types for model columns. Works similarly to Laravel's `casts` but for TypeScript.`#[TsType]`Custom cast classSpecify the TypeScript type for any model property that uses this custom cast class.`#[TsExclude]`Model class, accessor method, or relation methodExclude an entire model, specific accessors, or relations from the TypeScript output. See [Excluding with TsExclude](#excluding-with-tsexclude).#### Examples using `#[TsCasts]` attribute

[](#examples-using-tscasts-attribute)

##### Using `#[TsCasts]` attribute on `casts()` method

[](#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 &amp; model class name

[](#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

[](#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',
        '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;
    metadata: MetadataType | null;
    dimensions: ProductDimensions;
    created_at: string;
    updated_at: string;
}
```

#### Examples using `#[TsType]` attribute

[](#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

[](#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;
}
```

### PHPDoc Descriptions for Models

[](#phpdoc-descriptions-for-models)

Similar to enums, this package automatically reads PHPDoc doc blocks from your model classes and outputs them as JSDoc comments in the generated TypeScript interfaces. Descriptions are read from the following locations:

LocationSourceJSDoc PlacementModel classDoc block above the model classAbove the `export interface` declarationColumnsDoc block above the column's Attribute accessor methodAbove the column propertyMutatorsDoc block above the mutator's Attribute accessor methodAbove the mutator propertyRelationsDoc block above the relation methodAbove the relation propertyFor columns and mutators, the package looks for a doc block on the corresponding accessor method — either new-style (`protected function name(): Attribute`) or old-style (`public function getNameAttribute()`). The new-style accessor is checked first.

Lines starting with `@` (such as `@param`, `@return`, `@phpstan-type`, etc.) are automatically filtered out.

```
/** Application user account */
class User extends Model
{
    /** User name formatted with first letter capitalized */
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn ($value): string => ucfirst((string) $value),
        );
    }

    /** User initials (e.g. "JD" for "John Doe") */
    protected function initials(): Attribute
    {
        return Attribute::make(
            get: fn (): string => collect(explode(' ', $this->name))
                ->map(fn (string $part) => strtoupper(substr($part, 0, 1)))
                ->implode(''),
        );
    }

    /** Polymorphic images (avatar gallery, etc.) */
    public function images(): MorphMany
    {
        return $this->morphMany(Image::class, 'imageable');
    }
}
```

Generated TypeScript using the `model-full` template:

```
import type { Image } from './';

/** Application user account */
export interface User
{
    // Columns
    id: number;
    /** User name formatted with first letter capitalized */
    name: string;
    email: string;
    // Mutators
    /** User initials (e.g. "JD" for "John Doe") */
    initials: string;
    // Relations
    /** Polymorphic images (avatar gallery, etc.) */
    images: Image[];
    images_count: number;
    images_exists: boolean;
}
```

### Timestamps as Date Objects

[](#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 ValueGenerated TypeScript Type`false``created_at: string``true``created_at: Date`### Custom TypeScript Type Mappings

[](#custom-typescript-type-mappings)

This package ships with a comprehensive set of PHP-to-TypeScript type mappings (e.g., `integer` → `number`, `boolean` → `boolean`, `json` → `object`). 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',   // 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.

### Output Options

[](#output-options)

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

Config KeyDefaultDescription`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](#enum-metadata-vite-plugin)) that need to know which PHP source files were collected so they can trigger a re-publish when those files change.

API Resources
-------------

[](#api-resources)

This package can generate TypeScript interfaces from your Laravel [API Resources](https://laravel.com/docs/eloquent-resources) (`JsonResource` classes). It statically analyzes the `toArray()` method to extract property names, types, and optionality — producing a TypeScript interface that matches the shape of your API responses.

By default, the package will look for resources in the `app/Http/Resources` directory. You can customize this with the `additional_resource_directories`, `included_resources`, and `excluded_resources` config options (see [Filtering Resources](#filtering-resources)).

### How It Works

[](#how-it-works)

The package uses PHP Parser to statically analyze each resource's `toArray()` method. It resolves property types by inspecting the backing Eloquent model's database schema and cast definitions. The backing model is determined from (in priority order):

1. The `#[TsResource(model:)]` attribute
2. The `@mixin` PHPDoc tag (resolved via use statements)
3. Convention-based guess — reverses Laravel's naming convention (`App\Http\Resources\UserResource` → `App\Models\User`)
4. `#[UseResource]` attribute scan — checks all collected models for a `#[UseResource(ResourceClass::class)]` attribute pointing to this resource (Laravel 12+ only)

Most resources only need `@mixin` or the naming convention. The `#[TsResource(model:)]` attribute is useful when the resource name doesn't match the model, and `#[UseResource]` handles cases where the resource lives outside the standard `Http\Resources` namespace.

### Supported Patterns

[](#supported-patterns)

The analyzer recognizes the following patterns inside `toArray()`:

#### Direct Property Access

[](#direct-property-access)

```
'id' => $this->id,
'name' => $this->name,
'status' => $this->status,       // Enum cast → generates enum type
```

Types are resolved from the model's database columns and cast definitions.

#### Conditional Methods

[](#conditional-methods)

All conditional methods produce **optional** properties (with `?` in TypeScript):

MethodDescriptionGenerated Type`$this->when(cond, value)`Include when condition is trueInferred from value`$this->whenHas('attr')`Include when attribute is presentFrom model column type`$this->whenNotNull($this->attr)`Include when not nullFrom model column type`$this->whenLoaded('relation')`Include when relation is loadedFrom model relation type`$this->whenCounted('relation')`Include when count is loaded`number``$this->whenAggregated('rel', 'col', 'fn')`Include when aggregate is loaded`number``$this->whenPivotLoaded('table')`Include when pivot is loaded`unknown`See [Nullable Relations](#nullable-relations) for `whenLoaded` nullability handling.

#### Enum Properties with `EnumResource`

[](#enum-properties-with-enumresource)

Use `EnumResource::make()` to expose enum-cast properties as rich enum objects:

```
'status' => EnumResource::make($this->status),
'currency' => EnumResource::make($this->currency),
```

When `enums_use_tolki_package` is enabled (the default), these generate `AsEnum` types with automatic imports. When disabled, they generate the enum's `Type` alias (e.g., `StatusType`).

#### Nested Resources

[](#nested-resources)

Reference other resources using `::make()`, `::collection()`, or `new`:

```
// Single nested resource (optional when inside whenLoaded)
'author' => UserResource::make($this->whenLoaded('user')),

// Using new instead of ::make() — works identically
'author' => new UserResource($this->whenLoaded('user')),

// Collection of nested resources
'tags' => TagResource::collection($this->whenLoaded('tags')),

// Non-conditional nested resource
'owner' => UserResource::make($this->user),
```

Both `SomeResource::make(...)` and `new SomeResource(...)` are fully supported and behave identically — the analyzer resolves the resource type, tracks the FQCN for imports, and detects conditional arguments for optionality.

Self-referencing resources are also supported:

```
'parent' => CategoryResource::make($this->whenLoaded('parent')),
'children' => CategoryResource::collection($this->whenLoaded('children')),
```

#### Merge Operations

[](#merge-operations)

Use `merge` and `mergeWhen` to spread additional properties into the response:

```
// Unconditional merge — properties are required (not optional)
$this->merge([
    'full_name' => $this->first_name . ' ' . $this->last_name,
    'total_display' => $this->total,
]),

// Conditional merge — properties are optional
$this->mergeWhen($this->is_featured, [
    'weight' => $this->weight,
    'dimensions' => $this->dimensions,
]),
```

Both `merge` and `mergeWhen` also accept closures and arrow functions instead of array literals:

```
// merge with closure
$this->merge(fn () => [
    'currency_label' => $this->currency,
]),

// mergeWhen with closure
$this->mergeWhen($this->paid_at !== null, fn () => [
    'shipped_at' => $this->shipped_at,
    'tracking' => $this->tracking_number,
]),
```

MethodOptionalityDescription`$this->merge([...])`RequiredProperties are always present`$this->mergeWhen(cond, [...])`Optional (`?`)Properties included conditionally#### Closure &amp; Arrow Function Values

[](#closure--arrow-function-values)

The analyzer resolves closures and arrow functions used as value arguments. Simple closures that return a single expression are analyzed recursively:

```
// Arrow function — return expression analyzed directly
'status' => $this->when(true, fn () => $this->status),

// Arrow function returning a nested resource
'user' => $this->when(true, fn () => UserResource::make($this->user)),

// Full closure — first return statement is analyzed
'notes' => $this->when(true, function () {
    return $this->notes;
}),
```

This works anywhere a value expression is expected — including `when`, `whenLoaded`, `whenNotNull`, `merge`, and `mergeWhen`.

#### Parent `toArray()` Spread

[](#parent-toarray-spread)

Extend a parent resource using `...parent::toArray($request)`. Parent properties appear first, and the child can override any key:

```
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'status' => EnumResource::make($this->status),
        ];
    }
}

class ApiPostResource extends PostResource
{
    public function toArray(Request $request): array
    {
        return [
            ...parent::toArray($request),
            'status' => $this->status,       // Overrides parent's EnumResource type
        ];
    }
}
```

The child `ApiPostResource` inherits all parent properties (`id`, `title`, `status`), with `status` overridden to use the plain enum value instead of `EnumResource::make()`.

If the parent itself extends `JsonResource` (the base class), the spread automatically delegates to the model's database attributes — see [JsonResource Base Delegation](#jsonresource-base-delegation).

#### Trait Method Spread

[](#trait-method-spread)

Spread trait method return values into `toArray()` with `...$this->traitMethod()`. The analyzer reads `@return array{key: type}` PHPDoc annotations to resolve property types:

```
trait IncludesMorphValue
{
    /**
     * @return array{morphValue: string}
     */
    protected function includeMorphValue(): array
    {
        return ['morphValue' => $this->resource->getMorphClass()];
    }
}

class PostResource extends JsonResource
{
    use IncludesMorphValue;

    public function toArray(Request $request): array
    {
        return [
            ...$this->includeMorphValue(),
            'id' => $this->id,
            'title' => $this->title,
        ];
    }
}
```

Generates:

```
export interface Post {
    morphValue: string;   // From trait PHPDoc
    id: number;
    title: string;
}
```

Multiline `@return` shapes are also supported:

```
/**
 * @return array{
 *     firstName: string,
 *     lastName: string,
 *     isActive: bool,
 * }
 */
protected function includeProfile(): array
{
    // ...
}
```

Another option for defining the return types of a trait method is to use the `#[TsResourceCasts]` attribute on the trait method itself with the same syntax as the `#[TsCasts]` attribute for models:

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsResourceCasts;

trait IncludesExtras
{
    #[TsResourceCasts([
        'location' => ['type' => 'GeoPoint', 'import' => '@/types/geo'],
        'flag' => ['type' => 'string | null', 'optional' => true],
        'extra' => 'Record',
    ])]
    protected function includeCastedExtras(): array
    {
        return [
            'location' => strtoupper('x'),
            'flag' => strtolower('y'),
        ];
    }
}
```

Tip

Trait spreads also flow through parent inheritance. If a parent resource spreads a trait method and a child extends it with `...parent::toArray($request)`, the child inherits the trait-contributed properties.

Note

When a trait method has no `@return array{...}` PHPDoc or `#[TsResourceCasts]` attribute, its properties will be typed as `unknown`.

#### JsonResource Base Delegation

[](#jsonresource-base-delegation)

Resources that have **no `toArray()` method** or whose `toArray()` simply returns `parent::toArray($request)` automatically generate properties from the backing model's database schema:

```
/**
 * @mixin User
 */
class UserResource extends JsonResource
{
    // No toArray() — properties auto-generated from User model
}
```

You can also spread the base properties and add computed keys:

```
/**
 * @mixin User
 */
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            ...parent::toArray($request),
            'full_name' => strtoupper($this->name),
        ];
    }
}
```

The model is resolved from `#[TsResource(model:)]`, `@mixin` PHPDoc, or use statements. When no model can be detected, the resource produces an empty interface.

#### Attribute Filters (`only` / `except`)

[](#attribute-filters-only--except)

Resources that use `$this->only([...])` or `$this->except([...])` to filter model attributes are supported — both as a direct return value and as a spread:

```
// As the return value
public function toArray(Request $request): array
{
    return $this->only(['id', 'name', 'email']);
}

// As a spread in a return array
public function toArray(Request $request): array
{
    return [
        ...$this->except(['password', 'remember_token']),
        'role' => EnumResource::make($this->role),
    ];
}
```

Both methods delegate to the backing model's full database schema and filter by the listed keys. Properties retain their original types from the model.

Note

Currently only `only` and `except` are supported as attribute filter methods. Other collection-style methods are not analyzed. If you find you need additional methods, open and issue, or better yet, submit a PR with the added functionality! [See FiltersModelAttributes](src/Analyzers/Concerns/FiltersModelAttributes.php)

#### Resource Collections

[](#resource-collections)

`ResourceCollection` subclasses are supported. The analyzer resolves `$this->collection` to the singular resource type as an array:

```
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'has_admin' => true,
        ];
    }
}
```

Generates:

```
import type { UserResource } from './';

export interface UserCollection
{
    data: UserResource[];
    has_admin: unknown;
}
```

The singular resource is resolved from:

1. **Explicit `$collects` property** — if defined on the collection class
2. **Naming convention** — `UserCollection` → `UserResource` (strips "Collection", appends "Resource")

```
class OrderCollection extends ResourceCollection
{
    // Explicit: use OrderResource as the singular resource
    public $collects = OrderResource::class;

    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
        ];
    }
}
```

When the singular resource cannot be resolved (e.g., `MiscCollection` with no matching `MiscResource`), `$this->collection` falls back to `unknown`.

Larger support for `ResourceCollection` features (e.g., pagination metadata, `additional()` method, etc.) may be added in a future release.

### Example

[](#example)

Given this resource:

```
use AbeTwoThree\LaravelTsPublish\EnumResource;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Models\User;

/**
 * User account resource.
 *
 * @mixin User
 */
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'role' => EnumResource::make($this->role),
            'profile' => $this->whenLoaded('profile'),
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            'phone' => $this->whenHas('phone'),
            'avatar' => $this->whenNotNull($this->avatar),
            'posts_count' => $this->whenCounted('posts'),
        ];
    }
}
```

The package generates the following TypeScript interface:

```
import { type AsEnum } from '@tolki/enum';

import { Role } from '../enums';
import type { Profile } from '../models';
import type { PostResource } from './';

/** User account resource. */
export interface UserResource
{
    id: number;
    name: string;
    email: string;
    role: AsEnum;
    profile?: Profile | null;
    posts?: PostResource[];
    phone?: string | null;
    avatar?: string | null;
    posts_count?: number;
}
```

Notice how:

- Direct properties (`id`, `name`, `email`) are **required**
- `whenLoaded`, `whenHas`, `whenNotNull`, and `whenCounted` properties are **optional** (`?`)
- `EnumResource::make()` generates `AsEnum` with the proper imports
- `PostResource::collection()` is typed as `PostResource[]`
- Bare `whenLoaded('profile')` resolves to the model relation type (`Profile | null`)
- PHPDoc class descriptions are preserved as JSDoc comments

### Resource Attributes

[](#resource-attributes)

Three attributes are available for configuring resource TypeScript generation:

AttributeTargetDescription`#[TsResource]`Resource classOverride the interface name, specify the backing model, or add a description`#[TsResourceCasts]`Resource class or methodOverride or add property types with custom TypeScript types`#[TsExclude]`Resource classExclude the entire resource from the TypeScript output.See [Excluding with TsExclude](#excluding-with-tsexclude)

#### `#[TsResource]` — Configure Resource Generation

[](#tsresource--configure-resource-generation)

Use this attribute to override the generated interface name, explicitly specify the backing model, or add a description:

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsResource;
use App\Models\User;

#[TsResource(name: 'UserData', model: User::class, description: 'User API response')]
class UserResource extends JsonResource
{
    // ...
}
```

ParameterTypeDefaultDescription`name``?string`Class nameOverride the TypeScript interface name`model``?class-string`Auto-detectedExplicitly specify the backing Eloquent model`description``string``''`Added as a JSDoc comment above the interfaceTip

When `name` is set, it also affects the output filename. For example, `#[TsResource(name: 'Address')]` generates `address.ts` instead of `address-resource.ts`.

#### `#[TsResourceCasts]` — Override Property Types

[](#tsresourcecasts--override-property-types)

Use this attribute to override inferred types or add virtual properties with custom TypeScript types:

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsResourceCasts;

#[TsResourceCasts([
    'metadata' => 'Record',
    'coordinates' => ['type' => 'GeoPoint', 'import' => '@/types/geo'],
    'flagged_at' => ['type' => 'string | null', 'optional' => true],
])]
class CommentResource extends JsonResource
{
    // ...
}
```

Each entry can be:

FormatExampleDescriptionPlain string`'Record'`Override the type onlyArray with `import``['type' => 'GeoPoint', 'import' => '@/types/geo']`Custom type with an import statementArray with `optional``['type' => 'string', 'optional' => true]`Override the type and mark as optionalProperties defined in `#[TsResourceCasts]` that don't exist in `toArray()` are appended to the generated interface. Properties that do exist have their types overridden.

Generated TypeScript with the `coordinates` example:

```
import type { GeoPoint } from '@/types/geo';

export interface CommentResource
{
    id: number;
    content: string;
    is_flagged: boolean;
    flagged_at?: string | null;
    metadata: Record;
    author?: UserResource;
    post?: PostResource;
    coordinates: GeoPoint;
}
```

##### On Trait Methods

[](#on-trait-methods)

`#[TsResourceCasts]` can also be applied to **trait methods** that are spread into `toArray()`. This lets you control types for trait-contributed properties without modifying the resource class:

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsResourceCasts;

trait IncludesLocation
{
    #[TsResourceCasts([
        'location' => ['type' => 'GeoPoint', 'import' => '@/types/geo'],
        'flag' => ['type' => 'string | null', 'optional' => true],
        'extra' => 'Record',
    ])]
    protected function includeLocation(): array
    {
        return [
            'location' => $this->coordinates,
            'flag' => $this->flag,
        ];
    }
}
```

The attribute works identically to the class-level version — overriding types, marking properties optional, adding imports, and appending new properties. Properties defined in the attribute that don't exist in the method's return array (like `extra` above) are appended.

### Nullable Relations

[](#nullable-relations-1)

When `whenLoaded('relation')` resolves a relation type, the package determines whether it should include `| null` based on the relation kind and the database schema.

This is controlled by the `nullable_relations` config option (enabled by default). The strategy for each relation type is:

Relation TypeStrategyDescription`HasOne`, `MorphOne`, `HasOneThrough``nullable`Always nullable — the related record may not exist`BelongsTo``fk`Checks the foreign key column's DB-level nullability`MorphTo``morph`Checks both the morph type and FK column nullability`HasMany`, `BelongsToMany`, etc.`never`Collection relations — typed as arrays, never nullFor example, a `BelongsTo` relation with a nullable foreign key:

```
// Migration: $table->foreignId('user_id')->nullable();

// Resource:
'user' => UserResource::make($this->whenLoaded('user')),
```

Generates `user?: UserResource | null` — optional (from `whenLoaded`) and nullable (from the nullable FK).

You can disable nullable relation detection globally:

```
// config/ts-publish.php
'nullable_relations' => false,
```

Or override the strategy for specific relation types using `relation_nullability_map`:

```
// config/ts-publish.php
'relation_nullability_map' => [
    \Illuminate\Database\Eloquent\Relations\HasOne::class => 'never',
],
```

Valid strategies are `'nullable'`, `'never'`, `'fk'`, and `'morph'`.

### Filtering Resources

[](#filtering-resources)

You can customize which resources are discovered using the same include/exclude pattern as models and enums:

```
// config/ts-publish.php

// Only publish these specific resources (leave empty to include all)
'included_resources' => [
    App\Http\Resources\UserResource::class,
    App\Http\Resources\PostResource::class,
],

// Exclude specific resources from publishing
'excluded_resources' => [
    App\Http\Resources\InternalResource::class,
],

// Search additional directories for resources
'additional_resource_directories' => [
    'modules/Blog/Http/Resources',
],
```

Tip

Like models and enums, include and exclude settings accept both fully-qualified class names and directory paths.

### Conditional Resource Publishing

[](#conditional-resource-publishing)

You can disable resource publishing entirely in the config file:

```
// config/ts-publish.php

'publish_resources' => false,
```

Or publish only resources using the command flag:

```
php artisan ts:publish --only-resources
```

The `--only-resources` flag cannot be combined with `--only-enums` or `--only-models`.

Extending Interfaces with `#[TsExtends]` &amp; Configs
------------------------------------------------------

[](#extending-interfaces-with-tsextends--configs)

The `#[TsExtends]` attribute allows you to specify that a generated TypeScript interface should extend one or more other interfaces. This is useful when this package's limitations doesn't include properties on your interfaces. Another use is for sharing common properties across multiple models or resources without duplication.

You can place the `#[TsExtends]` attribute on any model or resource class, their parent classes, or even on traits used by those classes. The specified interfaces will be included in the generated TypeScript `extends` clause for any class that has the attribute directly or inherits it from a parent class or trait.

The `#[TsExtends]` attribute can be place multiple times on the same class or trait to define multiple interfaces that should be extended. The interfaces specified in all `#[TsExtends]` attributes on the class and its parents/traits will be combined into a single `extends` clause.

The `#[TsExtends]` attribute accepts the following parameters:

ParameterTypeDefaultDescription`extends``string``required`The interface to use or the way it should be used.`import``?string``null`The import path for the extended interfaces.`types``string[]``[]`The names of the interfaces to import from the specified module.### Example usage of `#[TsExtends]`

[](#example-usage-of-tsextends)

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsExtends;

#[TsExtends('ExampleInterface', '@js/types/models')]
class User extends Model
{
    // This model's generated TypeScript interface will extend ExampleInterface from @js/types/models
}
```

The above will generate the following TypeScript interface for the `User` model:

```
import type { ExampleInterface } from '@js/types/models';

export interface User extends ExampleInterface
{
    // ... model properties
}
```

You can also specify the interface extension with TypeScript helpers like `Partial`, `Pick`, or `Omit`. Use the third argument to list the interfaces used in the extension for proper importing and the first argument will be output as-is in the generated TypeScript:

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsExtends;

#[TsExtends('Partial', '@js/types/resources', ['ExampleInterface'])]
#[TsExtends('Pick', '@js/types/resources', ['ModularInterface'])]
class UserResource extends JsonResource
{
    // This resource's generated TypeScript interface will extend Partial and Pick
}
```

The generated TypeScript interface for `UserResource` will look like this:

```
import type { ExampleInterface, ModularInterface } from '@js/types/resources';

export interface UserResource extends Partial, Pick
{
    // ... resource properties
}
```

### Global Interface Extensions

[](#global-interface-extensions)

In some cases, you may want all your models or resources to extend a common interface without having to add `#[TsExtends]` to each class. You can achieve this with the `ts_extends.models` and `ts_extends.resources` config options:

```
// config/ts-publish.php
'ts_extends' => [
    'models' => [
        'HasTimestamps',
        ['extends' => 'BaseFields', 'import' => '@/types/base'],
        ['extends' => 'Pick', 'import' => '@/types/audit', 'types' => ['Auditable']],
    ],
    'resources' => [
        ['extends' => 'BaseResource', 'import' => '@/types/base'],
    ],
],
```

With the above config, all generated model interfaces will extend `HasTimestamps`, `BaseFields`, and `Pick`, while all resource interfaces will extend `BaseResource`. The necessary imports will be included automatically.

Example model:

```
import type { BaseFields } from '@/types/base';
import type { Auditable } from '@/types/audit';

export interface User extends HasTimestamps, BaseFields, Pick
{
    // ... model properties
}
```

Example resource:

```
import type { BaseResource } from '@/types/base';

export interface UserResource extends BaseResource
{
    // ... resource properties
}
```

Excluding with `#[TsExclude]`
-----------------------------

[](#excluding-with-tsexclude)

The `#[TsExclude]` attribute lets you exclude specific items from the TypeScript output. This is especially useful when `auto_include_enum_methods` or `auto_include_enum_static_methods` is enabled and you want to opt out individual enum methods.

`#[TsExclude]` can be applied to:

TargetEffectEnum classEntire enum is excluded from collection and publishingEnum methodMethod is excluded from the TypeScript outputModel classEntire model is excluded from collection and publishingModel accessorMutator/accessor is excluded from the TypeScript outputModel relationRelation is excluded from the TypeScript outputResource classEntire resource is excluded from collection and publishingNote

`#[TsExclude]` always takes priority — even if you use attributes like `#[TsEnumMethod]` or `#[TsEnumStaticMethod]` on enum methods, the methods will be excluded.

### Excluding an entire enum, model, or resource

[](#excluding-an-entire-enum-model-or-resource)

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsExclude;

#[TsExclude]
enum InternalStatus: string
{
    case Pending = 'pending';
    case Processing = 'processing';
}

#[TsExclude]
class AuditLog extends Model
{
    // This model will not be published to TypeScript
}

#[TsExclude]
class InternalResource extends JsonResource
{
    // This resource will not be published to TypeScript
}
```

### Excluding specific enum methods

[](#excluding-specific-enum-methods)

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsExclude;

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

    // Included in TypeScript output
    public function label(): string
    {
        return match($this) {
            self::Active => 'Active',
            self::Inactive => 'Inactive',
        };
    }

    // Excluded from TypeScript output
    #[TsExclude]
    public function internalCode(): int
    {
        return match($this) {
            self::Active => 100,
            self::Inactive => 200,
        };
    }
}
```

### Excluding model accessors and relations

[](#excluding-model-accessors-and-relations)

```
use AbeTwoThree\LaravelTsPublish\Attributes\TsExclude;

class User extends Model
{
    // Excluded from TypeScript output
    #[TsExclude]
    protected function secretToken(): Attribute
    {
        return Attribute::make(
            get: fn (): string => 'hidden',
        );
    }

    // Excluded from TypeScript output
    #[TsExclude]
    public function auditLogs(): HasMany
    {
        return $this->hasMany(AuditLog::class);
    }
}
```

Casing Configurations
---------------------

[](#casing-configurations)

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

Config KeyApplies ToDefault`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

[](#relationship-case-style)

Controls relationship names in the generated model TypeScript interfaces:

```
// config/ts-publish.php

'relationship_case' => 'snake', // default
```

Config ValueRelationship `hasMany(Post::class)`CountExists`'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`](https://laravel.com/docs/eloquent-relationships#counting-related-models) features and are included in every generated model interface.

### Enum Method Case Style

[](#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 ValueMethod `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.

JSON Enum HTTP API Resource
---------------------------

[](#json-enum-http-api-resource)

This package ships with an `EnumResource` — a Laravel [JSON resource](https://laravel.com/docs/eloquent-resources) that transforms any PHP enum case into a flat, API-friendly array. It runs the enum through the same transformer pipeline used for TypeScript publishing, so every `#[TsEnumMethod]` or `#[TsEnumStaticMethod]` you've configured is automatically included.

The `EnumResource` class is useful when you need to send a single enum instance (e.g., a model's status) to the frontend as a rich object with resolved method values, rather than just the raw string or integer value.

### Basic Usage

[](#basic-usage)

In a controller or route:

```
use AbeTwoThree\LaravelTsPublish\EnumResource;
use App\Enums\Status;

return new EnumResource(Status::Published);
```

From another HTTP API resource to automatically transform an enum property on a model or collection of models:

```
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use AbeTwoThree\LaravelTsPublish\EnumResource;
use App\Enums\Status;
use App\Enums\MembershipLevel;

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            // Assuming "status" is a model property cast to the Status enum
            'status' => new EnumResource($this->status),
            // Can also create enum resources from any enum case, not just model properties
            'membership_level' => new EnumResource(MembershipLevel::Free),
        ];
    }
}
```

### Enum Case Instance Response

[](#enum-case-instance-response)

Response:

```
{
    "name": "Published",
    "value": 1,
    "backed": true,
    "icon": "check",
    "color": "green",
}
```

### Response Shape

[](#response-shape)

Every response includes these base keys:

KeyTypeDescription`name``string`The enum case name`value``string | int`The backed value, or the case name for unit enums`backed``bool`Whether the enum is a backed enumInstance methods (decorated with `#[TsEnumMethod]` or via the auto-include config setting) are flattened as top-level keys with the resolved value **for the specific case** passed to the resource. Static methods (decorated with `#[TsEnumStaticMethod]` or via the auto-include config setting) are included as top-level keys with the resolved value from the static method.

This allows the `EnumResource` to provide the same data as the published TypeScript enum when you call the `from` method from the `@tolki/enum` package on the enum with the matching case value.

### Unit Enums

[](#unit-enums)

Unit enums (enums without a backed type) are also supported. Since they have no backed value, the `value` key will equal the case `name` and `backed` will be `false`:

```
return new EnumResource(Role::Admin);
```

```
{
    "name": "Admin",
    "value": "Admin",
    "backed": false
}
```

### Relationship to TypeScript Publishing

[](#relationship-to-typescript-publishing)

The `EnumResource` uses the same `EnumTransformer` pipeline as the `ts:publish` command. This means:

- Only methods marked with `#[TsEnumMethod]` (or all public methods when auto-include is enabled) are included.
- Methods with required parameters but no `params` on the attribute are excluded.
- The `enum_method_case` config setting applies to the method key names in the response.

This ensures the JSON response shape is consistent with the TypeScript types generated by this package.

### Typing API Responses with `AsEnum`

[](#typing-api-responses-with-asenum)

The `@tolki/enum` package exports an `AsEnum` utility type that resolves the `EnumResource` JSON response shape for any published enum. This gives you full type safety when consuming enum API responses on the frontend.

```
import type { AsEnum } from '@tolki/enum';
import type { Status } from '@/types/enums';

// Full discriminated union of all cases
type StatusResponse = AsEnum;
// { name: 'Draft'; value: 0; backed: true; icon: 'pencil'; color: 'gray'; ... }
// | { name: 'Published'; value: 1; backed: true; icon: 'check'; color: 'green'; ... }
```

The optional second type parameter lets you pre-narrow to a specific case by value:

```
// Narrowed to a single case
type DraftResponse = AsEnum;
// { name: 'Draft'; value: 0; backed: true; icon: 'pencil'; color: 'gray'; ... }
```

Use it to type your API responses:

```
const response = await fetch(`/api/articles/${id}`);
const article: { id: number; status: AsEnum } = await response.json();

if (article.status.value === 0) {
    // TypeScript knows this is the Draft case
    console.log(article.status.icon); // 'pencil'
}
```

### Auto-Generated `Resource` Model Interface

[](#auto-generated-resource-model-interface)

When `enums_use_tolki_package` is enabled (the default), any model with enum-cast columns automatically gets a `{Model}Resource` companion set of interfaces. These interfaces replace each enum-backed property with `AsEnum`, so you don't have to compose `Omit` + `AsEnum` manually on model properties or mutators that are cast to enums.

For a Post model that casts the database columns `status`, `visibility`, and `priority` to enums, the publisher will generate a `PostResource` interface that looks like this:

```
export interface Post {
    id: number;
    title: string;
    content: string;
    status: StatusType;         // Original enum type
    visibility: VisibilityType | null; // Original enum type
    priority: PriorityType | null;     // Original enum type
}

// Auto-generated — no manual typing needed
export interface PostResource extends Omit
{
    status: AsEnum;
    visibility: AsEnum | null;
    priority: AsEnum | null;
}
```

Use it to type API responses that use the `EnumResource` class:

```
import type { PostResource } from '@js/types/data/models';

const response = await fetch('/api/posts/1');
const post: PostResource = await response.json();

post.status.value; // 0 | 1
post.status.icon;  // 'pencil' | 'check'
```

The interfaces are generated for both the `model-full` and `model-split` templates. In split mode, the template will create a `PostResource` interface for the properties interface and a `PostMutatorsResource` interface for the mutators interface, since mutators can also be enum-cast properties:

```
export interface PostResource extends Omit
{
    status: AsEnum;
    // ...
}

export interface PostMutators
{
    due_notice: DueAtNoticeType;
}

export interface PostMutatorsResource extends Omit
{
    due_notice: AsEnum;
}
```

Naming conflicts are handled automatically — if two enum FQCNs share the same base name, namespace-prefixed aliases are used for both the type and const imports (e.g., `AppStatus`, `CrmStatus`).

Modular Publishing
------------------

[](#modular-publishing)

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

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

```

For applications that use a modular architecture (e.g., [InterNACHI/modular](https://github.com/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

[](#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/data/
├── app/
│   ├── enums/
│   │   ├── role.ts
│   │   ├── membership-level.ts
│   │   └── index.ts
│   ├── models/
│   │   ├── user.ts
│   │   ├── order.ts
│   │   └── index.ts
│   └── http/
│       └── resources/
│           ├── user-resource.ts
│           ├── order-resource.ts
│           └── index.ts
├── accounting/
│   ├── enums/
│   │   ├── invoice-status.ts
│   │   └── index.ts
│   ├── models/
│   │   ├── invoice.ts
│   │   └── index.ts
│   └── http/
│       └── resources/
│           ├── invoice-resource.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

[](#how-it-works-1)

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

PHP ClassOutput 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

[](#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 ClassWithout Strip PrefixWith `'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

[](#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/data/app/models';
import { Invoice } from '@js/types/data/accounting/models';
import { InvoiceStatusType } from '@js/types/data/accounting/enums';
```

Extending &amp; Customizing the Pipeline
----------------------------------------

[](#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 StageConfig KeyDefault ClassResponsibilityCollector`model_collector_class``ModelsCollector`Discovers PHP model classesCollector`enum_collector_class``EnumsCollector`Discovers PHP enum classesCollector`resource_collector_class``ResourcesCollector`Discovers PHP resource classesGenerator`model_generator_class``ModelGenerator`Orchestrates transforming and writingGenerator`enum_generator_class``EnumGenerator`Orchestrates transforming and writingGenerator`resource_generator_class``ResourceGenerator`Orchestrates transforming and writingTransformer`model_transformer_class``ModelTransformer`Converts PHP class into TypeScript dataTransformer`enum_transformer_class``EnumTransformer`Converts PHP enum into TypeScript dataTransformer`resource_transformer_class``ResourceTransformer`Converts PHP resource into TypeScript dataWriter`model_writer_class``ModelWriter`Writes TypeScript model filesWriter`enum_writer_class``EnumWriter`Writes TypeScript enum filesWriter`resource_writer_class``ResourceWriter`Writes TypeScript resource filesWriter`barrel_writer_class``BarrelWriter`Writes barrel `index.ts` filesWriter`globals_writer_class``GlobalsWriter`Writes global declaration fileWriter`json_writer_class``JsonWriter`Writes JSON definitions fileWriter`watcher_json_writer_class``WatcherJsonWriter`Writes collected files JSON for watchersTemplate`model_template``model-split`Blade template for model outputTemplate`enum_template``enum`Blade template for enum outputTemplate`resource_template``resource`Blade template for resource outputTo 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.

Pre-Command Hook
----------------

[](#pre-command-hook)

If you need to run custom logic right before the `ts:publish` command executes — such as dynamically configuring directories, adjusting included/excluded models, or performing any setup that requires processing — you can register a pre-command hook using `callCommandUsing`.

This is useful because the closure is only executed when the `ts:publish` command actually runs, not at service provider boot time. This keeps your boot process fast and avoids unnecessary overhead on every request.

### Basic Usage

[](#basic-usage-1)

In your `AppServiceProvider` (or any service provider), register a closure in the `boot` method:

```
use AbeTwoThree\LaravelTsPublish\LaravelTsPublish;

public function boot(): void
{
    LaravelTsPublish::callCommandUsing(function () {
        // This only runs when `php artisan ts:publish` is executed
        config()->set('ts-publish.additional_model_directories', [
            'modules/Blog/Models',
            'modules/Shop/Models',
        ]);
        config()->set('ts-publish.additional_resource_directories', [
            'modules/Blog/Http/Resources',
            'modules/Shop/Http/Resources',
        ]);
    });
}
```

### Dynamic Directory Discovery

[](#dynamic-directory-discovery)

A common use case is using Symfony Finder to automatically discover module directories:

```
use AbeTwoThree\LaravelTsPublish\LaravelTsPublish;
use Symfony\Component\Finder\Finder;

public function boot(): void
{
    LaravelTsPublish::callCommandUsing(function () {
        $modelDirs = collect(Finder::create()->directories()->in(base_path('modules'))->name('Models')->depth(1))
            ->map(fn ($dir) => $dir->getRelativePathname())
            ->values()
            ->all();

        $enumDirs = collect(Finder::create()->directories()->in(base_path('modules'))->name('Enums')->depth(1))
            ->map(fn ($dir) => $dir->getRelativePathname())
            ->values()
            ->all();

        $resourceDirs = collect(Finder::create()->directories()->in(base_path('modules'))->name('Resources')->depth(2))
            ->map(fn ($dir) => $dir->getRelativePathname())
            ->values()
            ->all();

        config()->set('ts-publish.additional_model_directories', $modelDirs);
        config()->set('ts-publish.additional_enum_directories', $enumDirs);
        config()->set('ts-publish.additional_resource_directories', $resourceDirs);
    });
}
```

Note

Only one closure can be registered at a time. Calling `callCommandUsing` again will replace the previous closure.

Configuration Reference
-----------------------

[](#configuration-reference)

Below is a quick reference of all available configuration options:

Config KeyTypeDefaultDescription`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/data`Directory where TypeScript files are written`publish_enums``bool``true`Enable or disable enum publishing`publish_models``bool``true`Enable or disable model publishing`publish_resources``bool``true`Enable or disable resource publishing`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``nullable_relations``bool``true`Append `| null` to singular relation types based on smart detection`relation_nullability_map``array``[]`Override nullability strategy per relation type`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`nullDirectory 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`resources_namespace``string``'resources'`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`nullDirectory 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`nullDirectory 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`resource_template``string``laravel-ts-publish::resource`Blade template for resource 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`included_resources``array``[]`Only publish these resources (empty = all)`excluded_resources``array``[]`Exclude these resources from publishing`additional_resource_directories``array``[]`Extra directories to search for resourcesNote

The 20 pipeline class config keys (e.g. `model_collector_class`, `enum_writer_class`, `resource_transformer_class`, etc.) are listed in the [Extending &amp; Customizing the Pipeline](#extending--customizing-the-pipeline) section above.

See the [full configuration file](https://github.com/abetwothree/laravel-ts-publish/blob/main/config/ts-publish.php) for detailed comments on each option.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Abraham Arango](https://github.com/abetwothree)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

52

—

FairBetter than 96% of packages

Maintenance96

Actively maintained with recent releases

Popularity25

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity64

Established project with proven stability

 Bus Factor1

Top contributor holds 95.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

31

Last Release

32d ago

Major Versions

v0.0.11 → v1.0.02026-03-17

v1.5.0 → 2.x-dev2026-05-26

### Community

Maintainers

![](https://www.gravatar.com/avatar/097a4b9143e3e24a53a380590816ac98c66b90c61769be0c955d28289336779c?d=identicon)[abetwothree](/maintainers/abetwothree)

---

Top Contributors

[![abetwothree](https://avatars.githubusercontent.com/u/1899636?v=4)](https://github.com/abetwothree "abetwothree (375 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (13 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")

---

Tags

apieloquentenumsfrontendlaravelmodelsresourcestypescriptlaravelabetwothreelaravel-ts-publish

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/abetwothree-laravel-ts-publish/health.svg)

```
[![Health](https://phpackages.com/badges/abetwothree-laravel-ts-publish/health.svg)](https://phpackages.com/packages/abetwothree-laravel-ts-publish)
```

###  Alternatives

[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.1k9.9M90](/packages/dedoc-scramble)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.3M42](/packages/spatie-laravel-pdf)[wnx/laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.

213389.8k2](/packages/wnx-laravel-backup-restore)[spatie/laravel-model-flags

Add flags to Eloquent models

4471.2M4](/packages/spatie-laravel-model-flags)[clickbar/laravel-magellan

This package provides functionality for working with the postgis extension in Laravel.

438834.4k1](/packages/clickbar-laravel-magellan)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
