Passare da JavaScript a TypeScript non significa necessariamente riscrivere un progetto da zero. In molti casi è possibile effettuare la migrazione in modo graduale, convertendo progressivamente i file e introducendo la gestione dei tipi man mano che il progetto viene aggiornato.
L'obiettivo finale è arrivare a un progetto in cui TypeScript possa controllare in modo efficace il codice, individuando gli errori prima dell'esecuzione.
La strategia più semplice consiste nel convertire il progetto un file alla volta, invece di trasformare immediatamente tutti i file.
Un primo passaggio consiste generalmente nel rinominare i file: per esempio, da utenti.js a utenti.ts.
A questo punto TypeScript analizzerà il contenuto del file e potrebbe segnalare diversi errori.
Per esempio, prendiamo questo codice JavaScript:
function creaUtente(nome, eta) {
return {
nome: nome,
eta: eta
};
}Può essere trasformato in TypeScript:
function creaUtente(nome: string, eta: number) {
return {
nome,
eta
};
}Non è necessario aggiungere manualmente un tipo a ogni elemento: dove possibile, possiamo lasciare che TypeScript utilizzi la determinazione automatica dei tipi.
Durante la migrazione è normale trovare numerosi errori.
Possiamo utilizzare il compilatore per chiedere a TypeScript di controllare il progetto senza generare i file JavaScript (aggiungere npx davanti al comando se TypeScript è installato localmente):
tsc --noEmitGli errori possono riguardare, per esempio:
null o undefined. È importante risolvere gli errori invece di aggirarli sistematicamente utilizzando valori sconosciuti come any o Type Assertions.
Uno degli errori più comuni durante la conversione riguarda i parametri delle funzioni.
Ad esempio:
function saluta(nome) {
return `Ciao ${nome}`;
}Con una configurazione rigorosa, TypeScript può segnalare che nome ha un tipo any implicito.
Possiamo correggere il problema:
function saluta(nome: string) {
return `Ciao ${nome}`;
}Anche gli oggetti JavaScript possono richiedere una maggiore definizione della loro struttura.
Prendiamo come esempio questo codice:
function stampaUtente(utente) {
console.log(utente.nome);
console.log(utente.email);
}Questa funzione può essere resa più sicura definendo un'interfaccia:
interface Utente {
nome: string;
email: string;
}
function stampaUtente(utente: Utente) {
console.log(utente.nome);
console.log(utente.email);
}Ora TypeScript conosce esattamente la struttura prevista dall'argomento.
Durante il refactoring possiamo approfittare della migrazione per migliorare alcune strutture che in JavaScript erano poco definite.
Per esempio, potremmo aver utilizzato oggetti generici:
const utente = {
id: 1,
nome: "Andrea",
ruolo: "admin"
};Invece, possiamo definire una struttura riutilizzabile:
interface Utente {
id: number;
nome: string;
ruolo: string;
}Ora possiamo riutilizzare la struttura in più funzioni:
function stampaUtente(utente: Utente): void {
console.log(utente.nome);
}In questo modo la struttura dell'oggetto viene definita una sola volta.
Alcuni valori JavaScript possono assumere un numero limitato di valori.
Ad esempio, una variabile stato che indica lo stato di un utente:
const stato = "attivo";In un progetto JavaScript potremmo trovare controlli tramite if e else. In TypeScript possiamo descrivere esplicitamente i valori consentiti tramite le unioni di tipi:
type Stato = "attivo" | "inattivo" | "sospeso";Poi possiamo utilizzarlo in una funzione:
function aggiornaStato(stato: Stato) {
// ...
}Ora TypeScript impedisce di utilizzare valori non previsti, ad esempio aggiornaStato("eliminato");.
Durante una migrazione possiamo incontrare gruppi di valori rappresentati attraverso costanti o numeri. Ad esempio:
const ADMIN = 1;
const USER = 2;
const MODERATOR = 3;In determinati casi possiamo rappresentare questi valori attraverso un'enumerazione con enum:
enum Ruolo {
ADMIN = 1,
USER = 2,
MODERATOR = 3
}Possiamo quindi utilizzare Ruolo in una funzione:
function verificaRuolo(ruolo: Ruolo) {
// ...
}Gli enum sono utili quando vogliamo rappresentare un insieme strutturato di valori correlati. In altri casi, soprattutto quando i valori sono semplici stringhe o numeri, un'unione di tipi (che abbiamo visto nel paragrafo precedente) può essere un'alternativa più semplice.
Durante la migrazione possiamo anche trovare funzioni duplicate che eseguono la stessa operazione su tipi diversi.
Ad esempio:
function primoNumero(valori: number[]): number {
return valori[0];
}
function primaStringa(valori: string[]): string {
return valori[0];
}Possiamo utilizzare un Generic per creare una sola funzione:
function primo<T>(valori: T[]): T {
return valori[0];
}La funzione può quindi essere utilizzata con tipi diversi:
const numero = primo([10, 20, 30]);
const testo = primo(["uno", "due", "tre"]);TypeScript determina automaticamente il tipo di T. Questo permette di eliminare duplicazioni mantenendo contemporaneamente il controllo di TypeScript sui tipi.
Durante la migrazione possiamo incontrare librerie JavaScript che non forniscono direttamente i propri tipi.
Nota: abbiamo approfondito come gestire le librerie JavaScript nella lezione su moduli e librerie esterne in TypeScript. Qui abbiamo soltanto riassunto l'essenziale.
Per prima cosa possiamo verificare se esiste un pacchetto di definizioni:
npm install --save-dev @types/libreriaIn questo modo, verrà installato il pacchetto che comprende le definizioni dei tipi necessari per la libreria.