spatie/laravel-typescript-transformer
Convert PHP classes, enums, and more into TypeScript types automatically. Uses attributes to generate TS from your Laravel code, supports nullable and complex types, generics, and even TypeScript functions—keeping your backend and frontend types in sync.
Installation:
composer require spatie/laravel-typescript-transformer
Publish the service provider and config (if needed):
php artisan vendor:publish --provider="Spatie\LaravelTypeScriptTransformer\TypeScriptTransformerApplicationServiceProvider"
Basic Usage:
Annotate a PHP class with #[TypeScript] to generate a TypeScript type:
// app/Models/User.php
use Spatie\LaravelTypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
class User {
public int $id;
public string $name;
public ?string $address;
}
Run the transformer:
php artisan typescript:transform
Output (in resources/js/generated.d.ts by default):
export type User = {
id: number;
name: string;
address: string | null;
}
Watch Mode (for development):
php artisan typescript:watch
Automatically regenerates TypeScript on file changes.
Transform a DTO or Eloquent model to TypeScript for frontend API responses:
#[TypeScript]
class Post {
public int $id;
public string $title;
public Carbon $publishedAt;
public ?string $excerpt;
public Collection<Comment> $comments;
}
Generates:
export type Post = {
id: number;
title: string;
publishedAt: string; // Carbon auto-converts to string
excerpt: string | null;
comments: Comment[];
}
#[TypeScript] on classes to generate types.#[Ignore] or #[TypeScript(ignore: true)].#[TypeScript(name: 'CustomName')].Generate typed route helpers and controller actions:
#[TypeScript]
class PostController {
#[TypeScript]
public function index(Request $request): Collection<Post> { ... }
#[TypeScript]
public function store(StorePostRequest $request): Post { ... }
}
Output:
// Generated route helper
declare function route(name: 'posts.index', parameters?: { [key: string]: unknown }): string;
// Controller types
export type PostController = {
index: (request: Request) => Post[];
store: (request: StorePostRequest) => Post;
};
Transform PHP enums to TypeScript unions:
enum Status: string {
case PUBLISHED = 'published';
case DRAFT = 'draft';
}
Output:
export type Status = 'published' | 'draft';
Collection<T> to T[].Illuminate\Http\Request and form requests.{id: number}).laravel-dataIf using spatie/laravel-data, the transformer respects name_mapping_strategy.output:
// config/data.php
'name_mapping_strategy' => [
'output' => \Spatie\Data\Mapping\SnakeCaseMapper::class,
],
Output:
export type User = {
first_name: string; // Instead of camelCase
last_name: string;
}
Development:
typescript:watch to auto-generate types during development.resources/js/generated.d.ts (or custom path).Production:
typescript:transform in your build script (e.g., npm run dev).Testing:
$transformer = app(TypeScriptTransformer::class);
$result = $transformer->transform(new User());
Frontend Frameworks:
router.visit('/posts', { data: { posts: Post[] } });
API Clients:
Custom Transformers:
Spatie\TypeScriptTransformer\Transformers\Transformer for custom logic:
class CustomTransformer extends Transformer {
public function transformProperty(PropertyNode $property): string {
return match ($property->type) {
'App\Models\User' => 'UserType',
default => parent::transformProperty($property),
};
}
}
$this->app->bind(
TypeScriptTransformer::class,
fn () => new TypeScriptTransformer([new CustomTransformer()])
);
Circular Dependencies:
User referencing Post which references User).#[TypeScript(ignore: true)] or refactor.Carbon/DateTime:
Carbon instances convert to string. Override in a custom transformer:
public function transformProperty(PropertyNode $property): string {
if ($property->type === Carbon::class) {
return 'Date';
}
return parent::transformProperty($property);
}
Route Watcher Issues:
typescript:watch crashes with "null byte" errors, ensure no RouteFilter uses protected properties.Absolute Paths:
GlobalNamespaceWriter (e.g., /home/user/project/file.d.ts).generated.d.ts).Laravel-Data Conflicts:
laravel-data and the transformer conflict on naming, explicitly set:
#[TypeScript(name: 'UserDto')]
class User extends Data {}
Verbose Output: Enable debug mode in the service provider:
$this->app->singleton(TypeScriptTransformer::class, fn () => new TypeScriptTransformer([
new DtoTransformer(),
new CollectionTransformer(),
// ...
], debug: true));
Check Generated Files:
resources/js/generated.d.ts for errors.php artisan typescript:transform --verbose for detailed logs.Common Errors:
Class not found: Ensure the class is autoloaded (e.g., composer dump-autoload).TypeScriptTransformer not bound: Register the service provider in config/app.php.Exclude Files:
Use .typescript-transformer-ignore to skip files/directories:
# .typescript-transformer-ignore
app/Models/IgnoredModel.php
Custom Output Path: Configure in the service provider:
$this->app->singleton(TypeScriptTransformer::class, fn () => new TypeScriptTransformer(
transformers: [],
outputPath: __DIR__.'/../resources/js/types',
));
Format Generated Code: Integrate with Prettier or ESLint to auto-format:
$this->app->singleton(TypeScriptTransformer::class, fn () => new TypeScriptTransformer(
transformers: [],
writer: new GlobalNamespaceWriter(
outputPath: __DIR__.'/../resources/js/types',
formatter: new PrettierFormatter(),
),
));
Partial Transforms: Generate types for specific classes only:
php artisan typescript:transform --class="App\Models\User,App\Models/Post"
Performance:
laravel-mix or vite).Testing:
$transformer = Mockery::mock(TypeScriptTransformer::class);
$transformer->shouldReceive('transform')
->once()
->andReturn('export type User = { id: number }');
$this->app->instance(TypeScriptTransformer::class, $transformer);
HTTP Methods Filtering:
Customize controller action generation with httpMethodsPriority:
$this->app->singleton(Lar
How can I help you explore Laravel packages today?