Write raw TypeScript types directly in PHP attributes and generate .ts files with a single Artisan command.
This package ships a Laravel Boost skill. If you use Boost, run:
php artisan boost:installand 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.
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]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.
- PHP 8.1+
- Laravel 10, 11, or 12
composer require brunoscode/laravel-ts-annotationsPublish the config file:
php artisan vendor:publish --tag=ts-annotations-config// 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]',
],
];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,
];
}
}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']);
}
}#[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 {}#[TS(<<<'TS'
export type AdminDashboard = {
users_count: number;
revenue: number;
}
TS, output: 'admin')]
class DashboardController extends Controller {}# 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-runTip — heredoc indentation: Use PHP 7.3 flexible heredoc by placing the closing
TSmarker 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.
Types are written in this order within each output file:
- Class-level
#[TS]attributes, in the order the files are found during directory scan - 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 = { ... }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.
-
--watchflag for automatic regeneration on file change - Hybrid mode: infer TypeScript from PHP property types with
#[TSProp]overrides
composer install
vendor/bin/phpunitMIT — see LICENSE.