TypeScript
Qualidade de Software
Arquitetura
Boas Práticas
Liderança Técnica

TypeScript avanzato in pratica: cosa distingue l'utilizzo superficiale dall'utilizzo maturo

Mature TypeScript non riguarda la scrittura di più tipi, si tratta di lasciare che il compilatore lavori a tuo favore.

Adottare TypeScript è facile. Usarlo bene è raro. La differenza tra le due cose non appare il primo giorno, appare sei mesi dopo, quando il team deve effettuare il refactoring e scoprire se i tipi sono alleati o decorazioni.

C'è una superficie TypeScript, in cui tutto viene annotato manualmente, any appare ogni volta che le cose si fanno difficili e il compilatore viene utilizzato solo per completare automaticamente i nomi. Ed esiste un TypeScript maturo, in cui i tipi effettivamente modellano il problema e il compilatore diventa un instancabile revisore che rileva gli errori prima che il codice venga eseguito.

Questo testo riguarda il secondo. Non come un tutorial, ma come una discussione sul valore di ciascuna risorsa per la salute del codice e del team.

Deduzione: scrivere di meno per garantire di più

Il primo istinto di chiunque arrivi a TypeScript è quello di scrivere tutto. Ad ogni variabile, ad ogni ritorno, ad ogni parametro viene assegnato un tipo scritto a mano. Questo sembra zelante e in pratica è il contrario.

Un'eccessiva annotazione manuale crea rumore e, peggio ancora, crea bugie. Il tipo scritto a mano può differire dal valore effettivo e quindi è presente un'annotazione che documenta un intento che il codice non soddisfa più. L'inferenza non mente: deriva il tipo dal valore del fatto.

L'uso maturo si fida dell'inferenza dove è affidabile e riserva l'annotazione dove conta, ovvero i confini. Firma della funzione pubblica, contratto del modulo, formato dei dati che entra nel perimetro del sistema. In sostanza, lascia che sia il compilatore a dedurre. Meno codice, meno disaccordo, più verità.

Generici: la differenza tra riutilizzare il tipo e perdere il tipo

I generici sono spesso spaventosi perché la sintassi sembra accademica. Il concetto è semplice e profondamente pratico: è come scrivere qualcosa di riutilizzabile senza buttare via informazioni lungo il percorso.

Senza i generici, la soluzione più semplice per una funzione che funziona con qualsiasi cosa è digitare l'input e l'output come troppo generici e l'effetto è che il tipo viene perso. Chi richiama la funzione ottiene un valore informe e deve indovinare cosa farne.

Con i generici, la relazione tra input e output viene preservata. Una funzione che riceve un elenco di qualcosa restituisce lo stesso qualcosa e il compilatore lo sa al momento della chiamata. Questo è il cuore del riutilizzo sicuro: astrarre il comportamento senza astrarre le informazioni. I team che padroneggiano i farmaci generici scrivono meno utilità duplicate e ottengono il completamento automatico corretto in tutte.

Tipi di utilità: riutilizza il formato senza ripetere il formato

Ogni sistema accumula tipi che sono variazioni di altri tipi. La versione parziale di un oggetto per un form, la versione senza campo password da inviare al client, la versione di sola lettura di una configurazione.

L'approccio ingenuo consiste nel dichiarare nuovamente ogni variazione a mano. Il problema si presenta quando cambia il tipo originale: ora ci sono cinque copie da aggiornare e nessuna garanzia che te le ricordi tutte. È la stessa trappola del codice duplicato, solo nella forma del tipo.

I tipi utilitaristici risolvono questo problema derivando una forma da un'altra. Quando cambia il tipo di base, le variazioni seguono da sole, perché sono state definite in base ad esso. Ciò trasforma i tipi in un unico punto di verità ed elimina quella sottile classe di bug in cui il codice e il suo tipo sono stati aggiornati a velocità diverse.

Restringimento: insegna al compilatore a ragionare con te

Forse la caratteristica più sottovalutata è il restringimento, ovvero la capacità di TypeScript di restringere un tipo man mano che il flusso del codice avanza. Controlli se esiste un valore e all'interno di quel blocco il compilatore inizia a trattarlo come esistente.

Ciò cambia il modo in cui il team gestisce il caso limite. Invece di spargere controlli difensivi ovunque e sperare, modelli i possibili stati e lasci che il compilatore richieda che ciascuno venga gestito. Il valore può essere una cosa o un'altra e il codice viene compilato solo quando sono stati coperti entrambi i percorsi.

L’effetto culturale di questo è grande. Il nulla smette di essere una sorpresa produttiva e diventa un’esigenza del tempo di scrittura. Il team smette di scoprire di aver dimenticato un caso quando il cliente si lamenta e inizia a scoprirlo quando l'editor si lamenta. È la differenza tra un bug e un promemoria.

La guerra contro chiunque

any è la valvola di sfogo di TypeScript e, come ogni valvola di sfogo, è necessaria in dosi minime e distruttiva in dosi normali. Un any non si limita a disattivare il tipo di quella variabile. Contamina tutto ciò che tocca, perché tutto ciò che deriva da un any diventa anche un any.

Una base con diffusione any ha il peggio dei due mondi: paga i costi di configurazione e manutenzione TypeScript, ma perde le garanzie proprio nei punti più rischiosi, cioè dove qualcuno non sapeva scrivere e ha rinunciato.

I team maturi considerano il any come un segnale di avvertimento, non come una soluzione. Quando il tipo è veramente sconosciuto, esiste la possibilità sicura di contrassegnarlo come sconosciuto e forzare un controllo prima dell'uso anziché rilasciare tutto. La regola pratica è semplice: any dovrebbe essere una rara eccezione, giustificata e visibile in sede di revisione, mai la soluzione predefinita a un problema.

Modellare il dominio: il salto che paga di più

Il livello più alto di utilizzo di TypeScript] non è tecnico, è design. Utilizza i tipi per descrivere le regole aziendali in modo tale che lo stato non valido semplicemente non possa esistere.

Un ordine che può essere pagato o pendente, ma mai entrambi. Un utente che, quando invitato, non dispone ancora di determinati dati e, quando attivo, necessariamente li ha. Quando modelli questi stati come tipi distinti, il compilatore impedisce combinazioni impossibili prima dell'esecuzione di qualsiasi test.

Questo cambia la discussione della squadra. Invece di "questo campo può essere vuoto qui", la risposta è tipografica, esplicita e verificabile. Il dominio è documentato nel codice che viene eseguito, non in un documento che nessuno aggiorna. Per l'edge del sistema, dove i dati esterni arrivano senza alcuna garanzia, vale la pena combinarlo con la convalida runtime, e il percorso naturale è convalida dei dati con Zod, che collega i dati reali al tipo.

Sommando tutto, il rendimento non è estetico, è operativo. Meno bug che raggiungono la produzione, perché il compilatore li ha rilevati prima. Refactoring che il team affronta senza timore, perché la rottura di qualcosa appare subito. Documentazione che non invecchia, perché è il codice stesso.

Mature TypeScript non riguarda la scrittura di più caratteri. Si tratta di scrivere i tipi giusti nei posti giusti e lasciare che il compilatore svolga il noioso lavoro di verifica della coerenza, che è proprio il lavoro che gli esseri umani fanno male e le macchine fanno bene.

Se il tuo team utilizza già TypeScript ma tratta ancora i tipi come burocrazia, il passo successivo è alzare il livello della revisione del codice e trattare la qualità del tipo come parte della qualità del codice. Per vedere come questi concetti si collegano a un'architettura più ampia, vale la pena andare a monorepos with TypeScript.

Leggi anche