Introduzione a TypeScript Transformer 3
Due anni fa, un venerdì, ho iniziato a lavorare su un piano folle: prendere uno dei miei package PHP più utilizzati, eliminare ogni riga di codice e riscriverlo da zero.
Quella che inizialmente sembrava una cosa che poteva essere fatta in settimane ha finito per richiedere molto più tempo del previsto. Nel tempo necessario per rilasciare TypeScript Transformer 3, mia moglie è rimasta incinta, mia figlia ha compiuto un anno e c'è stato l'intero boom dell'AI, ops.
In questo post del blog esaminiamo cosa c'è di nuovo e perché abbiamo fatto una riscrittura!
Riscrivere il package da zero
Non capita tutti i giorni di decidere di riscrivere qualcosa da zero (almeno se non sei un agente di codifica LLM entusiasta). Nel caso di TypeScript Transformer la scelta è stata piuttosto semplice.
Mentre la versione precedente di TypeScript Transformer funzionava certamente, era davvero difficile estenderla. Il miglior esempio è probabilmente il caso d'uso più comune: combinare il package con Laravel Data.
Invece di utilizzare il supporto integrato per trasformare le classi in TypeScript e aggiungere qualche elemento extra per l'uso con gli oggetti Data, il package Laravel Data definisce la propria infrastruttura di trasformazione, considerando TypeScript Transformer solo come un sistema per trovare classi Data e fornirgli TypeScript grezzo. Questo significava che l'uso di alcune funzionalità fornite da TypeScript Transformer come nascondere proprietà, definire tipi TypeScript letterali e altro non funzionava.
In sostanza, la v2 di TypeScript Transformer aveva il seguente pipeline:
- Trovare classi che soddisfano una condizione
- Trasformare quelle classi in una rappresentazione testuale di TypeScript
- Se una classe ne riferisce un'altra, sostituire quel riferimento PHP con un riferimento TypeScript
- Scrivere tutto in un file
Nella v3 volevamo rielaborare l'intera idea e vedere TypeScript Transformer più come un framework per costruire un ponte tra PHP e TypeScript.
Cosa c'è di nuovo
Abbandonare i Collector
Nella versione precedente il flusso era macchinoso: avevi un Collector che cercava classi nel tuo codebase che soddisfacessero una certa condizione. I Collector potevano cercare specifiche classi Data, enum o classi taggate con un attributo #[TypeScript].
Tutte le classi trovate dal collector venivano poi passate a un trasformatore che convertiva la classe in TypeScript.
Nella v3 abbiamo eliminato il concetto di Collector. Prendiamo ogni classe nella tua applicazione, la passiamo attraverso tutti i transformer e il primo transformer che restituisce un oggetto Transformed valido vince.
Eliminando il concetto di Collector, cerchiamo di incoraggiare gli sviluppatori a creare il proprio transformer per soddisfare le proprie esigenze.
Di default, il package fornisce un ClassTransformer astratto che può essere configurato e un AttributedClassTransformer che trasformerà tutte le classi con un attributo #[TypeScript].
Più che classi
In precedenza ci concentravamo sulla trasformazione delle classi in tipi TypeScript. In questa versione è solo una piccola parte del package. Viene introdotto un nuovo concetto chiamato TransformedProvider, che ti permette di definire un oggetto Transformed, in parole povere: un pezzo di codice TypeScript. Può essere un tipo, un oggetto, una funzione o qualsiasi pezzo di codice TypeScript eseguibile. Ha un nome, una posizione in cui dovrebbe essere inserito (o un namespace TypeScript o un file di modulo) e può essere referenziato da altri oggetti Transformed.
I TransformedProvider creeranno questi oggetti Transformed; il flusso di trasformazione delle classi in cui le classi vengono passate attraverso i transformer è un esempio di TransformedProvider, ma ci sono molte possibilità. Ad esempio, un TransformedProvider potrebbe generare una funzione helper route di Laravel che crea URL di route in TypeScript come farebbe in PHP. Un altro potrebbe aggiungere tipi aggiuntivi come collezioni paginate al tuo codebase TypeScript, o condividere definizioni di permessi e valori di configurazione con il frontend.
PHPStan nel core
La prima versione di TypeScript Transformer è stata creata cinque anni fa. All'epoca poteva trasformare solo enum. Negli ultimi cinque anni abbiamo visto enormi miglioramenti al sistema di tipi di PHP e l'annotazione dei tipi è diventata il default. Tutto grazie a strumenti come PHPStan che forniscono informazioni cruciali sul funzionamento interno del tuo codice.
La versione 2 del package aveva già supporto per le annotazioni tramite il package PHPDocumentor. Sebbene PHPDocumentor sia stato aggiornato per nuovi costrutti come gli array shapes, manca del dettaglio e delle informazioni che il core di PHPStan fornisce per analizzare queste annotazioni.
Ecco perché abbiamo sostituito PHPDocumentor con il parser PHPDoc di PHPStan, permettendoti di tipizzare cose come:
Array Shapes
class CreateUserData {
/** @var array{name: string, email: string, age?: int} */
public array $payload;
}
Object shapes
class Config {
/** @var object{host: string, port: int, secure?: bool} */
public object $database;
}
Tipi generici
use Illuminate\\Support\\Collection;
class Team {
/** @var Collection<int, User> */
public Collection $members;
}
Valori e chiavi di enum
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
}
class Filter {
/** @var value-of<Status> */
public string $status;
/** @var key-of<Status> */
public string $statusKey;
}
Tipi costanti
class Endpoints {
public const DELETE = 'delete';
/** @var array{self::DELETE: string} */
public array $endpoints;
}
Un vero AST
Nelle versioni precedenti un transformer restituiva una semplice stringa di codice TypeScript. Sebbene questo facesse sicuramente il suo lavoro, creare rapidamente nuovi transformer o adattare l'output di un transformer era quasi impossibile.
Nell'ultima versione del package, un oggetto transformed contiene sempre un nodo che rappresenta l'AST di quel simbolo. Puoi ancora usare il nodo TypeScriptRaw per restituire l'intero codice come una singola stringa, ma il package fornisce più di 40 nodi integrati che rappresentano un sottoinsieme del linguaggio TypeScript.
Uno dei nodi più utili è il nodo TypeScriptReference. Contiene un riferimento a un altro oggetto transformed e verrà risolto quando i nodi vengono scritti in una rappresentazione testuale TypeScript.
Nelle versioni precedenti lo facevamo sostituendo stringhe %ReferenceName% nel codice TypeScript già scritto. Un nodo dedicato che rappresenta i collegamenti tra simboli è una grande vittoria!
Writer rinnovati
TypeScript Transformer viene fornito con due writer: un namespace writer che scrive tutti i simboli transformed basandosi sul loro namespace PHP in un unico file TypeScript che imita quei namespace, e un module writer che divide i namespace in file e inserisce i simboli transformed lì.
In questa versione del package permettiamo ai singoli transformer di memorizzare oggetti transformed in più writer, consentendoti di memorizzare tutti i tuoi tipi in un file namespace mentre mantieni il codice helper come l'helper route di Laravel in un modulo.
Abbiamo persino reso possibile referenziare simboli usando i nodi TypeScriptReference attraverso i writer. Quindi puoi referenziare un simbolo esportato da un modulo in un file scritto con namespace e viceversa.
Osservare l'evoluzione del tuo codice
Una delle funzionalità più interessanti di TypeScript Transformer 3 è sicuramente la modalità watch. Puoi avviarla in Laravel eseguendo php artisan typescript:transform --watch. Il package agnostico rispetto al framework ti permette di configurarla all'avvio del package.
La modalità watch utilizza un'architettura supervisor/worker: inizialmente viene avviato un supervisor che poi genera un worker. Il worker avvia un listener per gli aggiornamenti dei file nei percorsi che TypeScript Transformer sta osservando.
Quando qualcosa cambia, in realtà non possiamo fare affidamento sull'API di reflection di PHP per ottenere gli aggiornamenti al file. Per risolvere questo problema, leggiamo il file usando il package Roave Reflection e lo analizziamo in un oggetto reflection che può essere utilizzato in tutto il package per costruire oggetti transformed.
Ecco perché TypeScript Transformer non fornisce oggetti reflection ai transformer per costruire TypeScript. Abbiamo creato la nostra astrazione sopra le classi di reflection integrate di PHP e le classi di reflection di Roave, chiamata PhpNode. Esempi di tali nodi sono PhpClassNode, PhpPropertyNode, PhpMethodNode e altri. Usando le classi PhpNode, uno sviluppatore non deve mai ricordare con quale tipo di reflection sta lavorando. Funzionerà e basta.
Perché l'architettura supervisor/worker? Se per qualche motivo TypeScript Transformer si blocca, il worker si riavvierà automaticamente grazie al supervisor. Riproverà anche intelligentemente alcune volte prima di arrendersi.
Un altro motivo per cui abbiamo scelto questa architettura è che un worker vivrà sempre all'interno del tuo framework preferito. Diciamo che stai generando TypeScript per le route della tua applicazione. In Laravel possiamo chiamare php artisan route:list --json e ottenere tutte le route in formato JSON. Ora cosa succede se qualcuno aggiorna una route e non avessimo questa architettura? Poiché stiamo ancora eseguendo lo stesso processo dopo la modifica, non verranno prese in considerazione le modifiche quando si esegue di nuovo il comando route:list. Ecco perché i TransformedProvider hanno una via di fuga quando osservano le directory: possono inviare un segnale di escape, dicendo al worker di riavviarsi e quindi avere le route aggiornate disponibili quando si esegue di nuovo route:list.
Nel caso del codice per generare le route di Laravel, in realtà usiamo un altro approccio per aggiornare le route, poiché riavviare i worker ogni volta che qualcosa relativo alle route cambia può essere piuttosto pesante in termini di prestazioni. Siamo sicuri che ci siano molti altri casi d'uso come questo da scoprire e ottimizzare.
La modalità watch è in beta per ora, e ci aspettiamo molti bug, ma sarà divertente correggerli e ottimizzare la modalità per rendere lo sviluppo ancora più veloce!
Helper route di Laravel
TypeScript Transformer può comunicare con Laravel e recuperare tutte le route definite, permettendoci di generare un helper route che può essere usato in TypeScript:
import { route } from './helpers/route';
// Senza parametri
const indexUrl = route('users.index');
// https://laravel.dev/users
// Con parametri
const userUrl = route('users.show', { user: 1 });
// https://laravel.dev/users/1
Puoi configurare l'helper per escludere specifiche route dall'essere esposte al frontend.
Helper controller di Laravel
Probabilmente una delle mie funzionalità preferite che abbiamo costruito è una mappatura completa dal controller a TypeScript. L'abbiamo mostrata inizialmente 3 anni fa a Laracon US con una copia dell'helper action che abbiamo in Laravel ma poi utilizzabile in TypeScript:
action(['UserController', 'update'], {id: 1});
Sfortunatamente non abbiamo trovato un modo per farlo funzionare come volevamo a causa delle limitazioni nell'inferire array generici in TypeScript.
Nel frattempo è stato rilasciato un nuovo package Laravel che fornisce un'API elegante che abbiamo amato e che abbiamo esteso con funzionalità extra.
Nelle versioni precedenti di TypeScript Transformer, l'unico modo per comunicare gli endpoint dal backend al frontend era hardcodarli in oggetti dati che potevano poi essere inviati insieme ai dati al frontend. Sembrava un po' stupido costruire questi oggetti e aggiungevano un sacco di codice extra e dati sulla rete.
D'ora in poi, puoi importare un controller Laravel in TypeScript così:
import { PostsController } from './controllers';
Generare un URL e ottenere il metodo HTTP è ora semplice come:
const { url, method } = PostsController.index();
Il tuo endpoint richiede un parametro? Può essere passato così:
const { url, method } = PostsController.show({ post: 1 });
Poiché tutto è tipizzato, quando la firma di una route cambia, ad esempio viene aggiunto un parametro extra, il compilatore TypeScript si lamenterà assicurandosi che il tuo codice sia sempre corretto!
Non ci siamo fermati qui. Anche se non è comune aggiungere tipi di ritorno in Laravel, ti consigliamo di iniziare a farlo d'ora in poi.
Diciamo che tipizziamo una risposta del controller come un array:
class DashboardController
{
/** @return array{totalUsers: int, activeUsers: int, revenue: float} */
public function stats(): array
{
return [
'totalUsers' => User::count(),
'activeUsers' => User::where('active', true)->count(),
'revenue' => Order::sum('total'),
];
}
}
Ora puoi ottenere il tipo di risposta di questo controller nel tuo codice TypeScript in questo modo:
DashboardController.stats.Response // { totalUsers: number; activeUsers: number; revenue: number }
Usando Laravel Data, tipizzare le risposte è ancora più semplice:
class PostsController
{
public function show(string $post): PostData
{
return PostData::from(Post::findOrFail($post));
}
}
In TypeScript puoi trovare la risposta come PostsController.show.Response che ovviamente sarà PostData.
Usi Inertia? Ti copriamo noi. Anche se il tipo di risposta Inertia ufficialmente non supporta i generici (ancora?) possiamo tipizzarlo così:
class ProjectsController
{
/** @return \\Inertia\\Response<ProjectData> */
public function show(string $project): \\Inertia\\Response
{
return inertia('Projects/Show', ProjectData::from(
Project::findOrFail($project)
));
}
}
Ovviamente TypeScript Transformer è abbastanza intelligente da sapere che ProjectsController.show.Response è di tipo ProjectData.
Se usi Laravel Data per iniettare oggetti dati costruiti dalla richiesta, allora sei in una botte di ferro! Diciamo che il tuo controller assomiglia a questo:
class PostsController
{
public function store(StorePostData $data): PostData
{
return PostData::from(Post::create($data->all()));
}
}
TypeScript Transformer è abbastanza intelligente da sapere che il tipo di richiesta di questo endpoint del controller è StorePostData, permettendoci di scrivere questo bellissimo pezzo di codice:
const post: PostsController.store.Request = {
title: 'Hello world',
body: 'Lorem ipsum...',
};
const { url, method } = PostsController.store();
const response = await fetch(url, {
method,
body: JSON.stringify(post),
});
Non abbiamo supporto per Laravel request o Eloquent resources (per ora). Laravel Ranger è un nuovo package ufficiale di Laravel che dovrebbe essere in grado di tipizzare questi. Stiamo attualmente valutando se il loro output può essere mappato sul nostro sistema di tipi, il che renderebbe possibile trasformare anche questi. Restate sintonizzati più avanti quest'anno!
Modifiche minori
- Ora abbiamo un sistema di visitor che permette agli sviluppatori di visitare i nodi TypeScript generati, modificarli, aggiungerli o rimuoverli.
- L'attributo TypeScriptLiteral ora permette di importare altri simboli TypeScript dal tuo codebase, anche quelli non generati dal package
- Abbiamo rimosso la funzionalità di inline dei tipi TypeScript invece di referenziarli
Wayfinder
Mentre stavamo costruendo la versione tre del package, Laravel ha deciso di rilasciare il proprio framework per trasformare tipi e route al frontend. Penso che siano stati piuttosto ispirati dal nostro lavoro ma hanno preso una direzione diversa.
Fin dall'inizio, le richieste e le risorse di Laravel non sono tipizzate. Una richiesta è un insieme di regole di validazione mentre una risorsa è fondamentalmente solo un array che viene restituito. Nelle prime versioni abbiamo effettivamente provato a far funzionare questo, analizzando l'AST di PHP e cercando di costruire i tipi per le risorse e le richieste.
È fallito miseramente. Mentre funziona per casi semplici, non appena abbiamo creato risorse o richieste più complicate il parser si confondeva completamente e abbiamo buttato via quel codice.
Invece abbiamo costruito Laravel Data. Richieste e risorse tipizzate ci hanno permesso di essere rigorosi nel backend fornendo definizioni di tipo esatte che potevano essere usate nel frontend. Un'accoppiata perfetta!
Un altro svantaggio dell'approccio di Wayfinder è che analizzare gli AST PHP è lento ... davvero lento. Nella nostra modalità watch dobbiamo anche farlo per costruire gli oggetti reflection Roave. Abbiamo ottimizzato quella parte del nostro flusso così bene che non proveremo mai ad analizzare un AST che non dovrebbe essere analizzato. D'altra parte, TypeScript Transformer fa molto affidamento sulla reflection integrata di PHP che è velocissima.
Mentre noi e Wayfinder cerchiamo di risolvere lo stesso problema, sono piuttosto contento dell'approccio che abbiamo preso. In futuro speriamo ancora di usare i loro tipi analizzati di Laravel request e Eloquent resources, ma per ora continuiamo per la nostra strada!
TypeScript Transformer nell'era degli LLM agentici
Negli ultimi anni il nostro mondo è stato sconvolto dall'introduzione di agenti che scrivono codice per noi. C'è ancora un posto per un package come TypeScript Transformer?
Posso rispondere rapidamente con un sonoro sì! Ci sono alcune ragioni:
- TypeScript Transformer si basa su fatti, non ci sono allucinazioni, nessun contesto mancante. Il package ha un grafo di conoscenza di tutto il codice che dovrebbe essere condiviso tra frontend e backend e lo fa solo in base ai tipi forniti.
- La modalità watch rende tutto più facile: puoi avviarla, lasciare che i tuoi agenti rifattorizzino il codice PHP e automaticamente il TypeScript verrà aggiornato
- Non perderai un solo token per scrivere (o probabilmente per riscrivere) definizioni TypeScript, il che è carino e permette al tuo LLM di fare il lavoro che è davvero importante
Aggiornamento
Non esiste una guida all'aggiornamento dalla v2 poiché è un package completamente nuovo e non c'era modo di scriverne una.
Ti consigliamo di leggere la documentazione e provare a trasformare i tipi per vedere cosa si rompe.
Conclusione
Mi sono divertito un sacco a ricostruire questo package. È diventato esattamente quello che volevo: un nuovo modo per costruire ponti tra frontend e backend.
Puoi trovare TypeScript Transformer e il package companion Laravel entrambi su GitHub. La documentazione per entrambi i package si trova qui.
Dato che ora è stato rilasciato, inizierò a lavorare su laravel-data v5 che dovrebbe essere più facile da usare e molto più veloce, ci vediamo la prossima volta!
Altri articoli recenti
Laravel AI 1.1 e 1.2: Agent Skills, Cohere e structured output più solidi
Le release 1.1.0 e 1.2.0 di laravel/ai portano gli Agent Skills, l'approvazione obbligatoria per i tool built-in e nuovi provider come Cohere e Laya self-hosted. Sul fronte qualità arrivano i fix allo structured output per Bedrock e il supporto alle provider options. Aggiornati anche i modelli di default, con Claude 4.7+ che ignora i sampling params.
Laravel 13.35.0: scheduler più controllabile, code più flessibili e response Markdown
Laravel 13.35.0 porta Schedule::alwaysOnOneServer(), il supporto a schedule:interrupt per i comandi di lunga durata, --memory in percentuale per queue:work e il nuovo metodo markdown() sulla response factory. Tante piccole migliorie anche a test, view compilate in parallelo e dischi local.
BashBox: un interprete Bash in sandbox per agenti AI in PHP
BashBox è un interprete bash scritto in puro PHP che permette di eseguire gli script generati dagli agenti AI senza mai ricorrere a proc_open, exec o shell_exec. Ogni comando è una classe PHP che lavora su un filesystem virtuale, con limiti di esecuzione, quote disco e accesso di rete configurabili. Ideale per chi sviluppa agenti con il Laravel AI SDK o altre librerie simili.