\ /
Programming  Code Quality  typescript 

TypeScript Narrowing: come migliorare la gestione dei tipi con Type Guards e Conditional Types

Chi utilizza TypeScript avrà sicuramente apprezzato la sua capacità di individuare prematuramente potenziali bug nel codice, grazie alla deduzione automatica dei tipi delle variabili o delle funzioni in base al flusso del codice. Questa potente funzionalità, chiamata type narrowing, spesso opera dietro le quinte senza che ce ne accorgiamo.

Un Esempio Pratico

Immaginiamo il seguente scenario:

ts-narrowing_code-snippet_1.png

Se TypeScript non fosse in grado di restringere i tipi, si verificherebbe un errore nello statement if in quanto, se l'argomento strs fosse una stringa non sarebbe possibile chiamare il metodo join().

Grazie al type narrowing, invece, TypeScript capisce che all'interno dell'if, strs è un array di stringhe, permettendo così la chiamata al metodo join() senza problemi.

Gestione dello Stato di una Richiesta HTTP

Vediamo ora un esempio più complesso. Supponiamo di star lavorando su un'applicazione frontend e di voler tracciare lo stato di una richiesta HTTP all'interno del nostro store. Abbiamo i classici stati: idle, pending, success ed error. Per quest'ultimo vogliamo salvare il messaggio di errore al posto della semplice descrizione dello stato. Definiamo i tipi come segue:

ts-narrowing_code-snippet_2.png

Guardando la definizione del tipo RequestStatus potremmo chiederci come potremo andare ad identificare lo stato della richiesta senza fare giri eccessivamente complessi. In effetti, come vediamo nell'esempio che segue, verificare lo stato di una richiesta potrebbe diventare prolisso con questa struttura, dovendo fare un doppio controllo ad ogni verifica per aiutare TypeScript a restringere i tipi e non bloccarci nella compilazione.

ts-narrowing_code-snippet_3.png

Questo codice funziona, ma ripetere questi controlli in tutta l'applicazione è sia prolisso che difficile da mantenere.

Migliorare con una Funzione di Supporto

Proviamo allora ad implementare una funzione che ci aiuti a gestire il ritorno dello stato della richiesta corrispondente.

ts-narrowing_code-snippet_4.png

Utilizzando questa funzione, possiamo riscrivere il nostro codice precedente:

ts-narrowing_code-snippet_5.png

Tuttavia, TypeScript ha ancora difficoltà a dedurre il tipo di requestStatus dopo aver verificato il suo stato. Per noi è logico che se getRequestStatus ritorna RequestStatusEnum.ERROR, requestStatus contiene un oggetto con una proprietà error, ma TypeScript vede requestStatus ancora come un tipo RequestStatus, che può contenere anche stringhe.

Utilizzare le Type Assertions

Una soluzione potrebbe essere quella di suggerire esplicitamente a TypeScript il tipo della variabile tramite la type assertion.

ts-narrowing_code-snippet_6.png

TypeScript accetta questo suggerimento, ma questa soluzione rende il codice meno leggibile, meno robusto e viola il principio DRY (Don't Repeat Yourself), poiché dovremmo ripetere questa assertion ogni volta che controlliamo lo stato di una richiesta.

Un'Approccio Migliore: Type Guards e Conditional Types

L'ideale sarebbe lasciare a TypeScript il compito di dedurre il tipo corretto. Per metterlo in condizione di fare questo possiamo usare un type predicate.

ts-narrowing_code-snippet_7.png

Problema risolto!

Abbiamo scritto una funzione che ci permette verificare lo stato di una richiesta in modo coinciso e più elegante all'interno della nostra applicazione e, al contempo, di mettere Typescript in condizione di dedurre correttamente il tipo di stato contenuto in requestStatus nel caso in cui sia un errore.

Ora, scriviamo una funzione per ogni altro possibile stato della richiesta.

ts-narrowing_code-snippet_8.png

Questa soluzione è migliore, certo, ma ancora non convincente. Abbiamo creato una funzione specificha per ogni stato, quindi ogni funzione è fortemente accoppiata al suo stato e quindi poco flessibile. Se dovessimo modificare gli stati o la logica di verifica, dovremmo aggiornare ogni funzione. Inoltre abbiamo quattro nuove funzioni da ricordare, il che può sembrare banale, ma può diventare tutt'altro che scontato in applicazioni di grandi dimensioni con decine o forse centinaia di metodi, classi, costanti, tipi, ecc..

Combinare Type Guards e Conditional Types

Possiamo ridurre il numero di funzioni necessarie a una sola? Sì, possiamo farlo combinando altri strumenti che il buon Typescript ci mette a disposizione; i type predicate ed i conditional types.

Andiamo a crearla:

ts-narrowing_code-snippet_9.png

La funzione isRequestStatus verifica se lo stato della richiesta corrisponde a quello specificato e permette a TypeScript di effettuare il type narrowing correttamente.

Riscriviamo il nostro codice iniziale utilizzando questa nuova funzione.

ts-narrowing_code-snippet_10.png

Perfetto! Ora abbiamo una sola funzione tipizzata, leggibile, scalabile e manutenibile per la verifica dello stato.

Conclusione

In questo articolo, abbiamo esplorato come migliorare la gestione dello stato delle richieste HTTP in TypeScript utilizzando type guards e conditional types. Implementando queste tecniche, possiamo rendere il nostro codice più robusto e facile da mantenere, migliorando allo stesso tempo la sicurezza dei tipi. Ricordiamoci sempre che un buon uso dei tipi può fare la differenza tra un codice di qualità e ore spese a cercare bug difficili da individuare.

comments powered by Disqus