C'è un malinteso costoso nei team che hanno adottato TypeScript con entusiasmo. La convinzione che avendo tipi, il sistema sia protetto da dati non validi. Non lo è. E questa confusione tra cosa fa e cosa non fa il compilatore è l'origine di un'intera categoria di bug che compaiono solo in produzione.
Voglio dissipare direttamente questo malinteso, perché determina le decisioni architettoniche. Chiunque capisca dove finiscono le tipologie comincia a trattare i confini del sistema con la cura di cui hanno bisogno.
Cosa garantisce davvero TypeScript
TypeScript è un sistema di tipi che vive interamente in fase di compilazione. Scrivi le annotazioni, il compilatore le controlla e poi vengono cancellate. Il JavaScript eseguito in produzione non sa che quell'oggetto era un Usuario. Per il motore di esecuzione, è semplicemente un oggetto qualsiasi.
Questo è previsto dalla progettazione, non è un difetto. TypeScript è stato creato per non imporre alcun costo in fase di esecuzione. La conseguenza è che ogni garanzia di tipo è una promessa relativa al codice che hai scritto, non ai dati che elaborerà.
Finché i dati nascono all'interno del tuo codice, la promessa rimane valida. Una funzione che accetta un numero e restituisce un numero è protetta dal compilatore dall'inizio alla fine. Il problema inizia quando i dati arrivano dall’esterno.
Il confine dove evapora la garanzia
Pensa a tutto ciò che entra nel tuo sistema senza esserne creato. La risposta da un'API esterna. Il corpo di una richiesta HTTP. Un modulo compilato da un essere umano distratto. Una riga letta da database dopo una migrazione non riuscita. Una variabile d'ambiente. Un messaggio in coda.
In tutti questi punti, in genere fai qualcosa sulla falsariga di dichiarare che i dati che ricevi sono di un certo tipo. Un'affermazione. E qui sta l'errore: questa affermazione non verifica nulla. Dice semplicemente al compilatore di fidarsi di te e andare avanti. Se l'API ha modificato un campo da numero a testo, TypeScript continua a credere che sia un numero, perché gli hai detto di crederci.
Il risultato è un sistema che sembra digitato ma ha dei buchi proprio ai bordi, dove entra il mondo reale. L’errore non avviene al confine, dove sarebbe facile da diagnosticare. Succede a tre strati di profondità, quando qualcosa tenta di utilizzare quel campo come se la promessa fosse vera. Il percorso si raffredda, l'analisi dello stack punta nel posto sbagliato e qualcuno perde il pomeriggio.
La convalida in fase di esecuzione è ciò che mancava
La soluzione concettuale è semplice da enunciare e facile da differire: ai punti di ingresso è necessario verificare effettivamente, in fase di esecuzione, che i dati abbiano la forma prevista. Non fidarti, controlla.
Convalida runtime significa codice che esamina in modo efficace i dati e risponde se sono validi o meno. Se l'API prometteva un numero e inviava un testo, la convalida urla lì, sul bordo, con un messaggio chiaro, prima che il valore contamini il resto del flusso. Scambiate un bug silenzioso e profondo con un fallimento rumoroso e localizzato.
Storicamente, questo era laborioso e facile da dimenticare. Abbiamo scritto l'interfaccia TypeScript da un lato e, dall'altro, una funzione di convalida separata che controllava manualmente gli stessi campi. Due descrizioni della stessa cosa, tenute da persone diverse, in tempi diversi. Erano diversi. Differiscono sempre. Il tipo diceva una cosa, il validatore ne controllava un'altra e i dati effettivi seguivano una terza regola.
Zod e l'approccio schema-first
È qui che Zod cambia il modo in cui pensiamo al problema. L'idea centrale è invertire l'ordine: invece di scrivere il tipo e poi un validatore che cerca di stargli dietro, si scrive un unico schema, e da esso deriva automaticamente il tipo.
Descrivi la forma dei dati una volta, con le sue regole, i campi obbligatori, i formati e i limiti. Da questo schema Zod estrae due cose contemporaneamente. Uno è il validatore che viene eseguito in produzione ed esamina effettivamente i dati. L'altro è il tipo statico TypeScript], generato dalla stessa definizione, senza doverlo scrivere a mano.
Questo è il guadagno che conta: un’unica fonte di verità. Tipo e validazione non possono più divergere, perché provengono dallo stesso luogo. Se modifichi lo schema, anche il tipo cambia e qualsiasi codice che dipendeva dal vecchio formato non verrà compilato. La divergenza non è più possibile attraverso la costruzione, anziché essere evitata attraverso la disciplina.
I dati esterni entrano, passano attraverso lo schema ed escono dall'altra parte come un valore che il compilatore può ora trattare con fiducia legittima, perché la fiducia è stata guadagnata in fase di esecuzione e non solo dichiarata. L'asserzione cieca diventa un vero e proprio controllo e da quel momento in poi il resto del sistema viene nuovamente protetto dai tipi.
##Cosa cambia nella mente di chi decide
L'adozione della convalida schema-first non è una scelta dello strumento, ma una scelta di dove posizionare il confine di fiducia. La regola che propongo all'équipe è semplice: nulla che provenga dall'esterno entra senza passare attraverso uno schema. API, modulo, banca, coda, configurazione. Tutto convalida al limite.
All'interno di questo confine ci si fida completamente dei tipi, perché sono tornati a corrispondere alla realtà. Al di fuori di esso, non ti fidi di nulla che non sia stato verificato. Questa netta separazione tra il territorio convalidato e la natura selvaggia è ciò che rende il sistema prevedibile. Chiunque lavori con TypeScript in progetti avanzati sa che il testo è uno strumento di ragionamento e ragiona bene solo sui dati che effettivamente hanno la forma promessa.
Vale la pena registrare il costo, che è onesto e piccolo. È previsto uno sforzo iniziale per modellare gli schemi e un sovraccarico di esecuzione per la convalida dei dati ai margini. In cambio si elimina una classe di errori costosi proprio perché si manifestano tardivamente e lontani dalla causa. È uno dei migliori ritorni sull'investimento che conosco in termini di qualità del software e si riferisce direttamente a pratiche di sicurezza delle applicazioni Web più ampie, poiché la convalida dell'input è anche la prima linea di difesa.
Se guidi un team che ha adottato TypeScript ma tratta ancora i dati esterni con asserzioni cieche, vale la pena rivedere i confini del sistema questa settimana. Il costo per aggiungere la convalida nel punto in cui arrivano i dati è basso e il costo per non averla è esattamente il tipo di bug di cui nessuno vuole eseguire il debug il venerdì.
Leggi anche
- TypeScript avanzato in pratica: cosa distingue l'utilizzo superficiale dall'utilizzo maturo
- Monorepos con TypeScript: quando conta e cosa considerare in anticipo
- Perché TypeScript è diventato lo standard nel Web moderno
- Sicurezza di tipo end-to-end: dal banco al frontend senza superare i confini
- CRDT: come sincronizzare i dati serverless per arbitrare i conflitti
- Prestazioni del software: cosa insegnano i casi reali sulla qualità