Se avete mai programmato in JavaScript, saprete perfettamente che un programmatore ha un rapporto di affetto molto profondo nei confronti dei Type Error: dimentica il nome di una variabile, di che tipo sia, o non ricorda cosa va passato ad un metodo. Questo potrebbe non essere un grosso problema se parliamo di piccole applicazioni e piccoli team. Ma se il volume di codice comincia a crescere e, come spesso accade in React, i componenti iniziano a moltiplicarsi, allora questo rapporto d’amore comincia a barcollare e queste piccole sviste e dimenticanze iniziano a consumare un bel po’ del nostro tempo.
Vediamo un esempio in React (versione > 16.8), creato con Create React App. Nella nostra app avremo 4 funzionalità: visualizzare la lista, aggiungere un nuovo elemento, eliminare un elemento dalla lista e visualizzare il paese di riferimento di quella valuta cliccando su un elemento.
Ogni valuta ha la seguente struttura:
{
id: 3,
name: 'RUB',
value: 81.18,
at: new Date()
}
Nel dettaglio: name è il nome della valuta, value è il valore corrispondente ad 1 euro, at è la data in cui è stato registrato quel valore.
Lo stato dell’applicazione è gestito, tramite useState, in questo modo:
const [currencies, setCurrencies] = useState(data);
const [newCurrency, setNewCurrency] = useState({
id: data.length,
name: '',
value: 1,
at: null
});
const [info, setInfo] = useState('');
Immaginate di essere una nuova leva del team e di leggere questo snippet di codice per la prima volta. Indipendentemente dal vostro livello di scrupoli, vi dovrebbero sorgere almeno due domande:
Perché in fondo è questo il problema: quando abbiamo a che fare con stringhe, numeri e booleani, è tutto bellissimo, riusciamo ad applicare mentalmente una type inference e a dedurre il tipo di queste variabili. Ma per null cosa possiamo fare?
Vediamo un altro esempio con il form per l’aggiunta di una nuova valuta:
function AddItem(props) {
return (
<form onSubmit={props.handleSubmit}>
<h1>Aggiungi nuova valuta</h1>
<div>
<label>Nome</label>
<div>
<input
name="name"
value={props.item.name}
onChange={props.handleChange}
type="text"
/>
</div>
</div>
<div>
<label>Valore corrispondente ad 1 EURO</label>
<div>
<input
name="value"
value={props.item.value}
onChange={props.handleChange}
type="number"
min={1}
step={0.01}
/>
</div>
</div>
<button type="submit">Aggiungi</button>
</form>
)
}
Anche per i non appassionati di mistero, quel props lì in cima dovrebbe incuriosire un bel po’.
Questi problemi non sono nuovi, naturalmente, e si possono limitare in diversi modi: in minima parte già con un buon IDE, oppure utilizzando un linter o con la libreria prop-types, tanto per citarne alcuni.
Per fortuna esiste anche TypeScript. Indipendentemente da tutte le funzionalità che offre allo sviluppatore, ai suoi vantaggi e anche ai suoi svantaggi - che non tratteremo in questo breve articolo e che andrebbero considerati in base al progetto e al team di cui si dispone - concentriamoci solo su una delle tagline che questo superset di JavaScript offrirebbe:
TypeScript makes code more readable and debuggable
Ovvero: codice più leggibile e più facile da debuggare. Proviamo a riscrivere la nostra applicazione, stavolta utilizzando TypeScript. Lanciamo il comando:
npx create-react-app example2 --template typescript
che farà già tutta la configurazione al posto nostro. Ora creiamo un file types.d.ts in /src, e definiamo un tipo Currency per renderlo disponibile in tutta l’applicazione.:
type Currency = {
id: number
name: string
value: number
at: Date | null
}
Nel nostro componente App lo stato sarà il seguente:
const [currencies, setCurrencies] = useState<Currency[]>(data);
const [newCurrency, setNewCurrency] = useState<Currency>({
id: data.length,
name: '',
value: 1,
at: null
});
const [info, setInfo] = useState('');
Se ora leggessimo il codice per la prima volta sapremmo perfettamente ogni variabile che cosa contiene e che cosa dovrà gestire. Per il nostro form potremmo dichiarare un tipo per le props del componente, il quale deve ricevere:
Entrambe le funzioni ricevono, in ingresso, un evento del form e non restituiscono nulla. Una bozza della definizione di AddItemProps potrebbe essere:
type AddItemProps = {
item: Currency,
handleChange: (event) => void
handleSubmit: (event) => void
}
Ma event lasciato così, senza un tipo, non va bene, né per noi né per TypeScript, che ci obbliga ad assegnare un tipo ad ogni input. Ragioniamoci un secondo:
A questo punto, riscriviamo le nostre props in questo modo:
type AddItemProps = {
item: Currency,
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void
}
Non c’è bisogno di continuare: risulta chiaro che ora possiamo definire i tipi degli ingressi e delle uscite dei nostri componenti, dei metodi e delle variabili in generale. Inoltre, TypeScript ci segnalerà eventuali sviste o ci aiuterà a ricordare nomi e proprietà.
Qui sotto, ad esempio, dopo il . ci segnala l’elenco delle proprietà dell’item che abbiamo definito una riga sopra con il tipo Currency.
In quest’altra immagine, invece, ci segnala che value non è una stringa ma un numero e che stiamo sbagliando ad inizializzarla.
Tutto fantastico, certo, ma potreste pensare: “Non è quello che faccio già con la libreria prop-types?”. Beh, no, c’è una differenza sostanziale. La libreria prop-types effettua un controllo runtime; TypeScript, invece, agisce in compile time. Insomma: ci aiuta mentre scriviamo il codice!