È uscito il Corso Java Completo
Torna al blog

Cos'è TypeScript e perché usarlo

Cos'è TypeScript e perché usarlo: tipi controllati prima di eseguire, inferenza, interface e type, union e narrowing, adozione graduale e costi reali.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

9 min di lettura

Passi venti minuti a cercare un errore e alla fine scopri che una funzione riceveva una stringa dove ti aspettavi un numero, oppure che un oggetto arrivato da un'API non aveva il campo che stavi leggendo. Sono bug banali, ma li scopri solo quando il codice gira — spesso in produzione. TypeScript nasce per spostare quel momento prima, mentre scrivi. In questo articolo trovi cos'è davvero, cosa non fa (e qui si concentrano i fraintendimenti), quanto costa adottarlo e quando conviene lasciar perdere.

Cos'è

TypeScript è JavaScript con l'aggiunta dei tipi: dichiari che forma hanno i dati, un controllore li verifica prima dell'esecuzione e poi li rimuove, producendo JavaScript normale.

Il punto da cui parte tutto è che TypeScript non è un linguaggio nuovo da imparare da zero. È JavaScript più uno strato di annotazioni. Ogni file JavaScript valido è anche TypeScript valido: puoi rinominare un .js in .ts e nella maggior parte dei casi funziona ancora. Se JavaScript non lo conosci ancora, il percorso giusto è imparare JavaScript da zero e solo dopo aggiungere i tipi — invertire l'ordine rende tutto più confuso.

function prezzoTotale(prezzo: number, quantita: number): number {
  return prezzo * quantita;
}

prezzoTotale(10, 3);     // 30
prezzoTotale("10", 3);   // errore prima di eseguire

La seconda chiamata in JavaScript non fallisce: produce "101010", perché moltiplicare una stringa fa cose strane. In TypeScript l'editor la segnala mentre la scrivi.

I tipi spariscono: la cosa più fraintesa

Questa è la parte che va capita prima di ogni altra, perché ci si sbatte contro tutti almeno una volta.

I tipi esistono solo durante lo sviluppo. Nel codice compilato non ce n'è traccia. Il compilatore li controlla, poi li cancella e produce JavaScript identico a quello che avresti scritto a mano. A runtime, il browser o Node non sanno nulla dei tuoi tipi e non verificano niente.

La conseguenza pratica è pesante: i dati che entrano nel programma dall'esterno non sono controllati da TypeScript. Una risposta di un'API, un file JSON, un parametro nell'URL, un modulo compilato dall'utente.

type Utente = { id: number; nome: string; email: string };

const risposta = await fetch("/api/utente/1");
const utente: Utente = await risposta.json();

console.log(utente.email.toLowerCase());

Questo codice compila senza una lamentela. Ma response.json() restituisce any: quell'annotazione : Utente è una promessa che fai tu al compilatore, non una verifica. Se l'API cambia e non manda più email, il programma esplode a runtime con il classico errore su undefined, esattamente come in JavaScript puro.

La soluzione è una validazione vera al confine del programma, con una libreria come Zod o Valibot, che controlla i dati davvero e da cui puoi derivare il tipo:

import { z } from "zod";

const UtenteSchema = z.object({
  id: z.number(),
  nome: z.string(),
  email: z.string().email(),
});

type Utente = z.infer<typeof UtenteSchema>;

const utente = UtenteSchema.parse(await risposta.json()); // fallisce subito se i dati non tornano

La regola da tenere: validi ai bordi (API, form, file, variabili d'ambiente), ti fidi dei tipi all'interno. Le variabili d'ambiente in particolare sono sempre stringhe o undefined, per quanto tu le dichiari diversamente.

L'inferenza: non devi annotare tutto

L'errore da principiante più comune è l'opposto di quello che ci si aspetta: non è annotare troppo poco, è annotare troppo.

TypeScript deduce i tipi da solo quasi ovunque. Scrivere quello che già sa è rumore che rende il codice più lungo e più difficile da modificare.

// Ridondante
const nome: string = "Edoardo";
const numeri: number[] = [1, 2, 3];
const doppi: number[] = numeri.map((n: number): number => n * 2);

// Sufficiente: i tipi sono identici, dedotti
const nome = "Edoardo";
const numeri = [1, 2, 3];
const doppi = numeri.map((n) => n * 2);

Dove le annotazioni servono davvero:

  • I parametri delle funzioni. Lì TypeScript non ha modo di indovinare.
  • I confini pubblici di un modulo o di una libreria, dove un tipo esplicito è anche documentazione.
  • I casi ambigui, per esempio un array che inizia vuoto: const righe = [] diventa any[], mentre const righe: string[] = [] dice cosa conterrà.

Il tipo di ritorno di una funzione di solito puoi ometterlo. Vale la pena scriverlo quando la funzione è lunga e vuoi che il compilatore ti avvisi se per sbaglio ne cambi il risultato.

I mattoni: interface, type, union

I tipi di base sono quelli che ti aspetti: string, number, boolean, null, undefined, gli array (string[]) e gli oggetti. Per dare un nome a una forma hai due strumenti.

interface Prodotto {
  id: number;
  nome: string;
  descrizione?: string; // opzionale
  readonly creatoIl: Date; // non riassegnabile
}

type Stato = "bozza" | "pubblicato" | "archiviato";

interface descrive la forma di un oggetto ed è estendibile. type è un alias per qualunque tipo, inclusi quelli che interface non sa esprimere, come una union. In pratica: interface per gli oggetti, type per tutto il resto. Nessuna delle due scelte rovinerà il progetto.

Le union ("bozza" | "pubblicato") sono la parte che dà più valore nel lavoro quotidiano, perché eliminano intere categorie di errori: una stringa scritta male non compila più. Quando hai una union, TypeScript pretende che tu gestisca ogni caso prima di usarla, e il modo in cui restringi i casi si chiama narrowing:

type Risultato =
  | { stato: "ok"; dati: string[] }
  | { stato: "errore"; messaggio: string };

function mostra(r: Risultato) {
  if (r.stato === "errore") {
    console.error(r.messaggio); // qui esiste solo messaggio
    return;
  }
  console.log(r.dati.length); // qui esiste solo dati
}

Il compilatore capisce da solo, dentro ogni ramo, quale variante hai in mano. È un modo di modellare i dati che rende impossibili gli stati assurdi, per esempio un risultato con un errore e dei dati validi insieme.

any e unknown

any disattiva il controllo su un valore. Tutto è permesso e non ricevi nessun avviso.

const dati: any = await risposta.json();
dati.utente.nome.maiuscolo(); // compila, e crolla a runtime

Usare any per far tacere un errore vanifica il senso di TypeScript, e il danno si propaga: ogni valore derivato da un any diventa a sua volta non controllato. Un codice pieno di any ha tutti i costi di TypeScript e nessuno dei benefici.

L'alternativa è unknown: significa "non so cosa sia", ma ti obbliga a verificare prima di usarlo.

const dati: unknown = await risposta.json();

if (typeof dati === "object" && dati !== null && "nome" in dati) {
  // qui puoi lavorarci in sicurezza
}

Quando davvero non hai alternative (una libreria vecchia senza tipi, una conversione che sai essere corretta), metti any con un commento che spiega perché. Un any motivato è accettabile; cinquanta any sparsi sono una resa.

Adottarlo su un progetto che esiste già

Non serve riscrivere niente. La conversione si fa a pezzi, e il modo indolore è questo:

  1. Installa TypeScript e genera la configurazione: npm install -D typescript e npx tsc --init.
  2. Nel tsconfig.json attiva "allowJs": true e lascia strict spento all'inizio.
  3. Rinomina in .ts un file alla volta, partendo da quelli con poche dipendenze (utility, funzioni pure).
  4. Aggiungi i tipi delle librerie che ne hanno bisogno (npm i -D @types/node, e simili).
  5. Quando la maggior parte del codice è convertita, accendi strict: true e sistema quello che salta fuori.

Su strict vale la pena essere netti: accendilo su ogni progetto nuovo. Senza, null e undefined restano ammessi ovunque e perdi la protezione da cui viene gran parte del valore. Su un progetto esistente è invece l'ultimo passo, non il primo, altrimenti ti ritrovi con centinaia di errori il primo giorno.

Se hai una copertura di test decente, la conversione è molto più tranquilla: i tipi trovano una classe di errori, i test ne trovano un'altra, e non si sostituiscono a vicenda. Vale la pena procedere in commit piccoli, così puoi tornare indietro su un singolo file.

Il costo reale

Chi lo propone tende a raccontare solo i vantaggi. I costi esistono e sono concreti.

AspettoJavaScriptTypeScript
Errori di tipoA runtime, se ti va bene in testPrima di eseguire
AutocompletamentoParziale, a intuitoPreciso, conosce le tue strutture
Rinominare in sicurezzaCerca e sostituisci, con pauraL'editor segue i riferimenti
Configurazione inizialeNessunatsconfig, tipi, strumenti di build
Tempi di buildNessun passaggio in piùDa secondi a minuti su progetti grandi
Curva inizialeUna o due settimane per stare comodi
Codice da scrivereMenoQualche riga in più

Aggiungo le due cose che fanno perdere ore e che nessuno dice: i messaggi di errore su tipi complessi sono lunghissimi e illeggibili (si leggono dall'ultima riga verso l'alto, dove di solito c'è la causa vera), e le librerie senza tipi ufficiali costringono a cercare un pacchetto @types/... o a scriverti le dichiarazioni a mano.

Quando conviene e quando no

Conviene su qualsiasi progetto che duri più di qualche settimana, su cui lavori in più persone o che abbia una logica non banale sui dati. Più il codice è grande e più a lungo vive, più i tipi ripagano. Sono anche un buon supporto al refactoring: cambi una struttura e il compilatore ti elenca tutti i punti da aggiornare.

Non conviene per uno script di 50 righe che lanci una volta, per un prototipo usa e getta, o per un pezzo di codice che andrà buttato tra una settimana. In quei casi la configurazione costa più di quanto renda. Se sei all'inizio e stai ancora prendendo confidenza con le basi, aggiungere i tipi subito raddoppia le cose da capire: meglio prima imparare a programmare e i fondamentali di JavaScript.

In sintesi

TypeScript è JavaScript con i tipi verificati prima dell'esecuzione, che poi spariscono nel codice compilato.

Le quattro cose da ricordare: i tipi non controllano niente a runtime, quindi i dati esterni vanno validati davvero con uno schema; l'inferenza fa gran parte del lavoro e annotare tutto è un errore da principiante; any annulla i benefici, unknown è quasi sempre la scelta giusta; strict acceso sui progetti nuovi, per ultimo su quelli esistenti.

Il beneficio vero non è evitare di sommare una stringa a un numero: è poter cambiare il codice senza paura, perché il compilatore ti dice cosa hai rotto.

E l'onestà finale: per uno script breve non serve. È uno strumento per il codice che deve durare, non una medaglia da mettere su ogni file.

Per il contesto in cui TypeScript si inserisce, vedi come scegliere lo stack per una web app. Per installare TypeScript e le librerie di tipi, il pezzo complementare è npm, yarn o pnpm: quale gestore scegliere.