Skip to content

BrunosCode/laravel-ts-annotations

Repository files navigation

laravel-ts-annotations

Write raw TypeScript types directly in PHP attributes and generate .ts files with a single Artisan command.

Laravel Boost

This package ships a Laravel Boost skill. If you use Boost, run:

php artisan boost:install

and select brunoscode/laravel-ts-annotations when prompted. The skill teaches your AI agent how to use #[TS] attributes, run ts:generate, and manage the generated output.


Quick start

Place the attribute on a class or on individual methods — whichever keeps your code cleaner:

// On a class (e.g. an API Resource)
#[TS(<<<'TS'
    export type UserResponse = {
        id: number;
        name: string;
        role: 'admin' | 'editor' | 'viewer';
    }
    TS)]
class UserResource extends JsonResource {}

// On individual controller methods
class UserController extends Controller
{
    #[TS(<<<'TS'
        export type UserListResponse = {
            data: UserResponse[];
            total: number;
        }
        TS)]
    public function index(): JsonResponse { ... }

    #[TS(<<<'TS'
        export type UserShowResponse = {
            data: UserResponse;
        }
        TS)]
    public function show(User $user): JsonResponse { ... }
}
php artisan ts:generate
// resources/js/types/generated.ts  ← generated automatically

// [ts-annotations:start]
// ⚠️  Auto-generated — do not edit between these comments.

import type { PageProps } from '@inertiajs/core'

// --- App\Http\Resources\UserResource ---
export type UserResponse = {
    id: number;
    name: string;
    role: 'admin' | 'editor' | 'viewer';
}

// --- App\Http\Controllers\UserController::index() ---
export type UserListResponse = {
    data: UserResponse[];
    total: number;
}

// --- App\Http\Controllers\UserController::show() ---
export type UserShowResponse = {
    data: UserResponse;
}
// [ts-annotations:end]

Why this package?

Most existing solutions either infer TypeScript from PHP types (losing union types, template literals, generics) or go through a Swagger/OpenAPI intermediary (indirect and verbose). This package lets you write real TypeScript in PHP attributes — no inference, no intermediate format.


Requirements

  • PHP 8.1+
  • Laravel 10, 11, or 12

Installation

composer require brunoscode/laravel-ts-annotations

Publish the config file:

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

Configuration

// config/ts-annotations.php

return [

    // Directories scanned recursively for #[TS] attributes.
    'scan' => [
        app_path('Http'),       // covers Resources, Controllers, Requests, Middleware
        // app_path('Data'),    // add more paths as needed
    ],

    // Output .ts files. The array key is referenced inside #[TS(output: 'key')].
    'outputs' => [
        'default' => [
            'path'    => resource_path('js/types/generated.ts'),
            'imports' => [
                // "import type { PageProps } from '@inertiajs/core'",
                // add any imports that must always appear in this file
            ],
        ],
        // 'admin' => [
        //     'path'    => resource_path('js/types/admin.ts'),
        //     'imports' => [],
        // ],
    ],

    // Comment markers that delimit the generated section.
    // Everything outside the markers is preserved on re-generation.
    'markers' => [
        'start' => '// [ts-annotations:start]',
        'end'   => '// [ts-annotations:end]',
    ],

];

Usage

Annotate a class

Place #[TS] above any class in a scanned directory. Useful for API Resources, Form Requests, DTOs, and any class whose shape maps to a single TypeScript type.

use Brunoscode\LaravelTsAnnotations\Attributes\TS;

#[TS(<<<'TS'
    export type UserResponse = {
        id: number;
        name: string;
        email: string;
        role: 'admin' | 'editor' | 'viewer';
    }
    TS)]
class UserResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'    => $this->id,
            'name'  => $this->name,
            'email' => $this->email,
            'role'  => $this->role,
        ];
    }
}

Annotate controller methods

Place #[TS] above individual methods to keep each type next to the action it describes. Types are written in declaration order.

use Brunoscode\LaravelTsAnnotations\Attributes\TS;

class UserController extends Controller
{
    #[TS(<<<'TS'
        export type UserListResponse = {
            data: UserResponse[];
            total: number;
            per_page: number;
        }
        TS)]
    public function index(): JsonResponse
    {
        return response()->json(UserResource::collection(User::paginate()));
    }

    #[TS(<<<'TS'
        export type UserShowResponse = {
            data: UserResponse;
        }
        TS)]
    public function show(User $user): JsonResponse
    {
        return response()->json(new UserResource($user));
    }

    #[TS(<<<'TS'
        export type UserStoreResponse = {
            data: UserResponse;
            message: string;
        }
        TS)]
    public function store(StoreUserRequest $request): JsonResponse
    {
        $user = User::create($request->validated());
        return response()->json(['data' => new UserResource($user), 'message' => 'Created']);
    }
}

Define multiple types on the same class or method

#[TS] is repeatable — stack it as many times as needed:

#[TS(<<<'TS'
    export type UserResponse = {
        id: number;
        name: string;
    }
    TS)]
#[TS(<<<'TS'
    export type UserCollection = {
        data: UserResponse[];
        total: number;
        per_page: number;
    }
    TS)]
class UserResource extends JsonResource {}

Target a specific output file

#[TS(<<<'TS'
    export type AdminDashboard = {
        users_count: number;
        revenue: number;
    }
    TS, output: 'admin')]
class DashboardController extends Controller {}

Run the generator

# Generate all output files
php artisan ts:generate

# Generate only one specific file
php artisan ts:generate --output=admin

# Preview what would be written without touching any file
php artisan ts:generate --dry-run

Tip — heredoc indentation: Use PHP 7.3 flexible heredoc by placing the closing TS marker at the same indentation level as the type body. PHP strips that many leading spaces from every line, giving you clean zero-based indentation in the output.


Ordering in the output file

Types are written in this order within each output file:

  1. Class-level #[TS] attributes, in the order the files are found during directory scan
  2. Method-level #[TS] attributes, sorted by line number within each class

The source is always noted in a comment above each type:

// --- App\Http\Resources\UserResource ---
export type UserResponse = { ... }

// --- App\Http\Controllers\UserController::index() ---
export type UserListResponse = { ... }

File preservation

The generator only touches the section between the two marker comments. Everything outside the markers — manual imports, custom types, hand-written utilities — is left untouched on every run.

// My manual import — never overwritten
import type { CustomHelper } from './helpers'

// [ts-annotations:start]
// ⚠️  Auto-generated — do not edit between these comments.
// Generated at: 2026-05-10 12:00:00

import type { PageProps } from '@inertiajs/core'

// --- App\Http\Resources\UserResource ---
export type UserResponse = { ... }
// [ts-annotations:end]

// My local type — never overwritten
export type LocalState = 'idle' | 'loading' | 'error'

If a file doesn't exist yet, it is created from scratch. If it exists but has no markers, the generated block is appended at the end.


Roadmap

  • --watch flag for automatic regeneration on file change
  • Hybrid mode: infer TypeScript from PHP property types with #[TSProp] overrides

Testing

composer install
vendor/bin/phpunit

License

MIT — see LICENSE.

About

Generate TypeScript types from PHP attributes in Laravel

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages