Queste sono le note della lezione del corso su TypeScript. Nella lezione su JavaScript abbiamo incontrato un bel po’ di trappole: il + che concatena quando vorremmo sommare, 10 - undefined che fa NaN, le proprietà scritte male che valgono undefined senza che nessuno protesti. Sono tutti errori che il linguaggio lascia passare e che scopriamo solo a runtime. Magari in produzione. Magari di venerdì.
TypeScript nasce per spostare quella scoperta il più indietro possibile, cioè al momento in cui scriviamo il codice. È un soprainsieme di JavaScript: qualsiasi codice JS valido è già TypeScript valido, a cui si aggiungono le annotazioni di tipo. Un compilatore, tsc, controlla i tipi e produce JavaScript normale. Vale la pena fissare subito un concetto a cui torneremo: a runtime i tipi non esistono più. Servono a noi e agli strumenti mentre scriviamo; il browser continua a eseguire JavaScript.
Per provare gli esempi vi suggerisco il TypeScript Playground: scrivete a sinistra e vedete gli errori e il JavaScript generato a destra.
Annotazioni di base
L’annotazione si scrive dopo il nome, con i due punti:
let nome: string = "Paolino";
let prezzo: number = 9;
let disponibile: boolean = true;
prezzo = "nove"; // Error: Type 'string' is not assignable to type 'number'.
Il let a = 12 che poi diventa stringa e poi booleano, visto nella lezione su JavaScript, qui non compila più: una variabile nasce con un tipo e se lo tiene.
DON’T: annotare l’ovvio
TypeScript è bravissimo a dedurre i tipi da solo, e si chiama type inference:
let prezzo = 9; // prezzo è number, senza bisogno di dirlo
L’annotazione let prezzo: number = 9 è rumore: il valore parla da solo. Le annotazioni servono dove l’inferenza non può arrivare, cioè i parametri delle funzioni e i confini con il mondo esterno (le risposte delle API, per esempio). Vedremo entrambi tra poco.
DON’T: any
Esiste un tipo che spegne tutti i controlli:
let qualunque: any = 9;
qualunque = "nove"; // nessun errore
qualunque.toUpperCase(); // nessun errore
qualunque.affetta(); // nessun errore, esploderà a runtime
Ogni any è una porzione di programma in cui siamo tornati a JavaScript puro, e il compilatore smette di aiutarci proprio dove servirebbe di più. Vi capiterà di vederlo usato come scorciatoia per “far smettere di lamentarsi il compilatore”: è la versione TypeScript del copiare gli header CORS da Stack Overflow.
Due note pratiche. La prima: se davvero non conoscete ancora il tipo di un valore, esiste unknown, che al contrario di any vi obbliga a controllare cosa c’è dentro prima di usarlo. La seconda: nel file di configurazione tsconfig.json esiste l’opzione strict, che accende i controlli più rigorosi. Su un progetto nuovo partite sempre da strict: true; allentare un vincolo dopo è facile, recuperare un progetto cresciuto senza controlli molto meno.
Tipizzare gli oggetti
Qui TypeScript inizia a ripagarsi. Descriviamo una volta per tutte com’è fatta una pizza:
type Pizza = {
nome: string;
prezzo: number;
ingredienti: string[];
sconto?: number;
};
Il punto interrogativo su sconto la rende opzionale: una pizza può averla o no, ma se c’è dev’essere un numero. Ora possiamo dichiarare oggetti di tipo Pizza:
const margherita: Pizza = {
nome: "Margherita",
prezzo: 9,
ingredienti: ["Farina", "Pomodoro", "Mozzarella"],
};
console.log(margherita.prezo);
// Error: Property 'prezo' does not exist on type 'Pizza'. Did you mean 'prezzo'?
Quel “Did you mean ‘prezzo’?” è il motivo per cui sono qui a farvi questa lezione. In JavaScript margherita.prezo vale undefined, il programma prosegue e l’errore emerge tre funzioni più in là, sotto forma di NaN. In TypeScript non compila, con tanto di suggerimento.
Esiste anche la parola chiave interface per descrivere la forma di un oggetto, e per gli usi di questa lezione le due sono equivalenti. Le differenze emergono in casi più avanzati di cui riparleremo; per ora scegliete type e andate avanti sereni.
Funzioni tipizzate
Si annotano i parametri e, volendo, il tipo di ritorno:
function calcolaPrezzo(prezzo: number, sconto = 0): number {
return prezzo - sconto;
}
console.log(calcolaPrezzo(10, 2)); // 8
console.log(calcolaPrezzo(10)); // 10, lo sconto ha il default
calcolaPrezzo(10, "2"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
calcolaPrezzo(); // Error: Expected 1-2 arguments, but got 0.
Il 10 - undefined che in JavaScript produceva NaN qui è impossibile per costruzione: o passate un numero, o non compila. Notate anche che sconto = 0 non ha bisogno di annotazione, perché il tipo lo deduce dal default.
Le arrow functions si annotano allo stesso modo:
const benedici = (nome: string, cognome: string): string =>
`Io ti benedico ${nome} ${cognome}`;
Array tipizzati
Il tipo di un array si scrive con le parentesi quadre dopo il tipo degli elementi: string[], number[] e, naturalmente, Pizza[]:
const pizze: Pizza[] = [
{
nome: "Margherita",
prezzo: 9,
ingredienti: ["Farina", "Pomodoro", "Mozzarella"],
},
{
nome: "Marinara",
prezzo: 5,
ingredienti: ["Farina", "Pomodoro", "Aglio", "Origano"],
},
{
nome: "Contadina",
prezzo: 19,
ingredienti: ["Farina", "Melanzane", "Peperoni", "Mozzarella"],
},
];
I metodi visti nella lezione su JavaScript sono identici, ma ora lavorano su tipi noti:
const economiche = pizze.filter(x => x.prezzo < 10); // economiche è Pizza[]
const nomi = pizze.map(x => x.nome); // nomi è string[]
const totale = pizze.reduce((acc, x) => acc + x.prezzo, 0); // totale è number
Dentro le arrow, x è una Pizza: l’editor autocompleta le proprietà mentre scrivete, e x.prezo non compila. Tutto questo senza aggiungere una sola annotazione, perché l’inferenza propaga i tipi lungo la catena.
Union e literal types
Un tipo può essere l’unione di più alternative, e le alternative possono essere anche valori letterali. Lo stato di un ordine della pizzeria è l’esempio perfetto:
type StatoOrdine = "in-preparazione" | "in-forno" | "consegnato";
let stato: StatoOrdine = "in-forno";
stato = "annullato"; // Error: Type '"annullato"' is not assignable to type 'StatoOrdine'.
In JavaScript questi stati sarebbero stringhe qualsiasi, e un refuso come "in-fornoo" vivrebbe felice fino al primo bug report. Qui l’insieme dei valori leciti fa parte del tipo.
L’unione funziona anche tra tipi diversi, e si accompagna al narrowing: dentro un controllo, TypeScript restringe il tipo a quello compatibile con il ramo in cui ci troviamo.
function trovaPizza(id: number | string) {
if (typeof id === "number") {
// qui id è number, possiamo farci aritmetica
} else {
// qui id è string, possiamo chiamare toUpperCase
}
}
Generics, prima conoscenza
Chiudiamo con il pezzo più astratto della lezione, prendendola alla larga dal problema concreto. Il frontend della pizzeria carica il menu dalle API, e vorremmo una funzione riutilizzabile per le chiamate. Senza generics avremmo due scelte sbagliate: scrivere una funzione per ogni tipo di risposta, o farne una sola che restituisce any e rinunciare a tutto quello che abbiamo costruito finora.
I generics sono la terza via: la funzione dichiara un segnaposto di tipo, T, e chi la chiama dice cosa c’è dentro.
async function getJson<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
const pizze = await getJson<Pizza[]>("https://api.awesomepizza.com/pizzas");
// pizze è Pizza[]: filter, map e autocompletamento funzionano come prima
Una sola funzione, riusabile per il menu, gli ordini e qualsiasi altra risposta, senza perdere i tipi. Quando vedete <T> leggetelo così: “questa funzione lavora con un tipo che deciderà chi la usa”.
E qui serve una nota di onestà, che si riaggancia al concetto di inizio lezione: stiamo dichiarando che il server risponderà con un array di Pizza, ma TypeScript non può verificarlo, perché a runtime i tipi non esistono. Se il backend un giorno rinomina un campo, il compilatore non potrà salvarci. La validazione dei dati che arrivano da fuori è un tema che merita una lezione a parte; per ora vi basta sapere che il confine esiste e dove passa.
Per approfondire
- Il TypeScript Handbook - la documentazione ufficiale, scritta bene
- TypeScript Playground - per sperimentare e vedere il JavaScript generato
- La documentazione di tsconfig - per capire cosa accende strict