
Molti sviluppatori JavaScript credono di avere già tutto sotto controllo. Hanno gestito var
, dominato async/await
, sopravvissuto a callback infernali, conoscono ogni trucco del DOM e pensano di sapere tutto sul linguaggio. Poi arriva TypeScript, e ti senti improvvisamente un principiante. Non è solo JavaScript con tipi; è un ecosistema intero fatto di vincoli, errori a compile-time e regole da rispettare.
La differenza principale sta nella tipizzazione. In JavaScript, tutto è dinamico: puoi cambiare il tipo di una variabile a runtime e il linguaggio lo permette senza protestare. In TypeScript, ogni variabile e funzione può avere un tipo dichiarato, e il compilatore controlla tutto prima di eseguire il codice. Questo aumenta la sicurezza ma introduce frustrazione se non sei abituato.
Differenze fondamentali tra JavaScript e TypeScript
Quando passi da JavaScript a TypeScript, la prima cosa che noti è che improvvisamente il tuo editor è pieno di linee rosse. Benvenuto nel mondo dei tipi statici, dove il compilatore è il tuo nuovo migliore amico... o il tuo peggior nemico, dipende dal giorno.
JavaScript è stato progettato per essere flessibile, permissivo, quasi anarchico. Puoi sommare una stringa con un numero e ottenere "42"
, puoi chiamare una funzione con tre parametri quando ne accetta due, puoi assegnare undefined
a una variabile che prima conteneva un oggetto. TypeScript arriva e dice: "No, fermi tutti, qui servono delle regole".
La tabella sopra riassume le differenze chiave, ma la verità è che TypeScript non è solo "JavaScript + tipi". È un cambio di mentalità. Devi pensare al codice come a un contratto: ogni funzione promette di accettare certi tipi e restituire altri tipi specifici. Rompi il contratto? Il compilatore ti punisce con errori che sembrano geroglifici egizi.
Aspetto | JavaScript | TypeScript |
---|---|---|
Tipi | Dinamici | Statici (opzionali ma consigliati) |
Compilazione | Non necessaria | Necessaria per generare JavaScript |
Interfacce | Non presenti | Supportate e obbligatorie per strutture complesse |
Generics | Non esistono | Permettono funzioni e classi parametriche |
Controlli runtime | Solo runtime | Compile-time e runtime (tipi eliminati a runtime tramite type erasure) |
Tipizzazione e sicurezza
TypeScript ti obbliga a pensare prima di scrivere codice. Ad esempio, in JavaScript puoi fare:
let x = 5;
x = "ciao"; // perfettamente legale
In TypeScript, se dichiari il tipo:
let x: number = 5;
x = "ciao"; // errore: string non assegnabile a number
Il compilatore ti ferma sul nascere, evitando bug futuri. Questo è il bello… e anche il rompicapo.
Ecco il punto: in JavaScript, questa libertà ti sembra fantastica finché non passi tre ore a debuggare perché da qualche parte nel codice una funzione riceve una stringa invece di un numero e crasha misteriosamente. In TypeScript, queste tre ore le passi all'inizio a litigare col compilatore, ma poi il codice funziona.
La sicurezza dei tipi è come l'assicurazione sulla macchina: ti sembra inutile finché non hai un incidente. TypeScript è il casco obbligatorio della programmazione: un po' scomodo, ma ti salva la vita quando le cose vanno male. E credetemi, le cose vanno sempre male in JavaScript.
Funzioni e parametri
JavaScript è permissivo con i parametri:
function somma(a, b) {
return a + b;
}
somma(1, "2"); // "12", nessun errore
TypeScript richiede coerenza:
function somma(a: number, b: number): number {
return a + b;
}
somma(1, "2"); // errore a compile-time
Questo significa meno sorprese a runtime ma più pensiero preventivo.
La bellezza (o l'orrore) di JavaScript è che puoi chiamare somma(1, "2")
e ottenere "12"
. Oppure chiamare somma(1)
e ottenere NaN
perché b
è undefined
. O ancora meglio, chiamare somma(1, 2, 3, 4, 5)
e JavaScript usa solo i primi due parametri ignorando il resto. È caos puro.
TypeScript dice basta. Ogni parametro deve avere un tipo, ogni funzione deve dichiarare cosa restituisce. Vuoi parametri opzionali? Usa ?
. Vuoi parametri di default? Dichiaralo esplicitamente. Vuoi un numero variabile di parametri? Usa il rest operator con il tipo corretto. Non ci sono scorciatoie, non ci sono zone grigie. Solo contratti chiari e compilatori spietati.
All'inizio ti sembra una prigione. Dopo qualche mese, capisci che è libertà: libertà di refactorare senza paura, libertà di cambiare una funzione sapendo che il compilatore ti segnalerà tutti i posti dove rompi il codice.
Generics: il lato oscuro dei dev
In TypeScript i generics sono utilissimi ma fanno impazzire:
function wrap<T>(value: T): { value: T } {
return { value };
}
const numero = wrap(42); // T inferito come number
const testo = wrap("ciao"); // T inferito come string
Ora prova a mescolare tipi diversi o interfacce complesse, e il compilatore inizierà a inviarti errori criptici. Questo è il momento in cui impari cosa significa "rompicapo".
I generics sono il livello boss di TypeScript. Quando li incontri per la prima volta, sembrano magici: una funzione che funziona con qualsiasi tipo mantenendo la sicurezza dei tipi. Poi inizi a usarli seriamente e scopri l'inferno dei constraint, dei conditional types, e delle mapped types.
Vuoi creare una funzione che accetta un array di qualsiasi tipo e restituisce il primo elemento? Facile. Vuoi che quella funzione funzioni anche con tuple preservando il tipo esatto? Preparati a scrivere <T extends readonly unknown[]>
. Vuoi fare un Pick
custom che estrae solo le proprietà di tipo stringa da un'interfaccia? Benvenuto nel mondo di T[K] extends string ? K : never
.
Il bello (o il terribile) è che i generics sono ovunque nelle librerie moderne. Apri React e trovi useState<T>
, useRef<T>
, FC<Props>
. Apri una libreria di utility e trovi Partial<T>
, Record<K, V>
, ReturnType<T>
. Se non capisci i generics, non capisci metà del codice che usi ogni giorno.
E gli errori? Oh, gli errori. Quando sbagli qualcosa con i generics, TypeScript ti tira fuori messaggi tipo: Type 'string' is not assignable to type 'T extends keyof U ? U[T] : never'
. A quel punto apri Stack Overflow e preghi che qualcuno abbia già affrontato lo stesso incubo.
Interfacce e oggetti complessi
In JavaScript puoi creare oggetti liberamente:
const user = { name: "Luca", age: 30 };
user.id = 123; // perfetto
In TypeScript devi rispettare l'interfaccia:
interface User {
name: string;
age: number;
}
const user: User = { name: "Luca", age: 30 };
user.id = 123; // errore: id non definito in User
Questo ti obbliga a progettare dati e strutture con anticipo.
Le interfacce sono la fondazione di TypeScript. Ti costringono a pensare alla forma dei tuoi dati prima di iniziare a scrivere codice. In JavaScript, gli oggetti sono blob amorfi che crescono e cambiano a piacimento. Aggiungi una proprietà qui, cancellane una là, cambia il tipo di un'altra. È libertà creativa... o caos totale, dipende da come la vedi.
TypeScript introduce ordine. Vuoi un oggetto User? Prima definisci cos'è un User. Quali proprietà ha? Di che tipo sono? Sono tutte obbligatorie o alcune sono opzionali? Una volta definita l'interfaccia, ogni oggetto User nel tuo codice deve rispettare quelle regole. Non puoi più aggiungere proprietà a caso, non puoi più cambiare i tipi al volo.
Il vantaggio? L'autocomplete diventa magico. Scrivi user.
e il tuo editor ti mostra esattamente cosa puoi fare. Niente più typo stupidi tipo user.nmae
invece di user.name
. Niente più undefined is not a function
perché hai chiamato un metodo che non esiste. Il compilatore ti protegge dai tuoi stessi errori.
Lo svantaggio? Devi pensare. Devi progettare. Devi decidere prima cosa vuoi costruire. Per alcuni dev è liberatorio, per altri è una tortura. Ma una volta che ci prendi la mano, non torni più indietro.
Librerie JavaScript vs TypeScript
In JavaScript puoi importare qualsiasi libreria:
import _ from "lodash";
In TypeScript devi spesso aggiungere i tipi:
import _ from "lodash"; // serve @types/lodash
Senza i tipi, il compilatore ti urla contro. Questo è uno dei motivi per cui TypeScript può sembrare un rompicapo, soprattutto all'inizio.
Ecco un problema pratico che ogni dev TypeScript incontra: le librerie JavaScript non hanno tipi nativi. Lodash è scritto in JavaScript puro. jQuery è JavaScript puro. Migliaia di librerie NPM sono JavaScript puro. E TypeScript? TypeScript vuole i tipi.
La community ha risolto creando DefinitelyTyped, un repository gigantesco di definizioni di tipo per librerie JavaScript. Quando installi una libreria, devi spesso installare anche il pacchetto @types
corrispondente: @types/lodash
, @types/node
, @types/react
. È un passaggio in più, un'altra cosa da ricordare, un'altra fonte di frustrazione quando i tipi sono obsoleti o incompleti.
Il vero incubo arriva quando usi una libreria JavaScript obscura che non ha definizioni di tipo. Puoi: 1) scrivere le definizioni tu stesso (buona fortuna), 2) usare any
ovunque e rinunciare alla sicurezza dei tipi, o 3) cercare un'alternativa TypeScript-friendly. Nessuna opzione è particolarmente allettante.
E poi ci sono le versioni. I tipi sono in @types/libreria
versione 2.3, ma la libreria è alla 2.5. I tipi non corrispondono più. Il compilatore ti urla contro. Tu bestemmi. È il ciclo della vita TypeScript.
Ma quando tutto funziona, quando i tipi sono aggiornati e precisi, l'esperienza di sviluppo diventa sublime. Autocomplete perfetto, refactoring sicuro, zero bug di runtime. Vale la pena soffrire? Dipende da te.
Null, undefined e coerenza dei tipi
JavaScript tratta null
e undefined
in modo permissivo:
let x;
x = null; // ok
x = undefined; // ok
In TypeScript, se hai abilitato strictNullChecks
, devi dichiarare esplicitamente che una variabile può essere null o undefined:
let x: number | null = null;
x = undefined; // errore
Questo riduce bug e comportamenti imprevisti, ma ti fa imprecare ogni tanto.
Tony Hoare, l'inventore del null reference, lo ha chiamato "il mio errore da un miliardo di dollari". In JavaScript, hai sia null
che undefined
, perché apparentemente uno non bastava. E puoi assegnare entrambi a qualsiasi variabile, in qualsiasi momento, senza preavviso. È un festival di Cannot read property of undefined
e null is not an object
.
TypeScript con strictNullChecks
attivato è spietato. Ogni variabile che potrebbe essere null
o undefined
deve dichiararlo esplicitamente con l'union type: string | null
, User | undefined
, number | null | undefined
. E prima di usare quella variabile, devi fare un type guard: controllare che non sia null o undefined.
function getName(user: User | null): string {
if (user === null) {
return "Guest";
}
return user.name; // qui user è garantito non-null
}
Il compilatore ti obbliga a gestire i casi edge. Non puoi più ignorare il problema sperando che tutto vada bene. Devi pensare: "Cosa succede se questa variabile è null?". È frustrante? Sì. È noioso? Assolutamente. Previene bug? Ogni. Singolo. Giorno.
L'optional chaining (?.
) e il nullish coalescing (??
) sono arrivati in soccorso, rendendo la gestione di null/undefined meno verbosa. Ma il principio rimane: TypeScript ti costringe a essere onesto con te stesso sui casi in cui le cose possono andare male.
Conclusione: il compromesso del dev
Se sai JavaScript, TypeScript non è un upgrade automatico: è un mondo nuovo, con regole, tipi e vincoli. Ti farà imprecare contro generics, interfacce e librerie senza tipi. Ti obbligherà a pensare prima di scrivere e a modellare dati in modo chiaro. Ma una volta padroneggiato, diventa uno strumento potente che ti salva da ore di debugging e bug impossibili.
TypeScript è il rompicapo che ti insegna disciplina, ma ti ricompensa con sicurezza e codice manutenibile. Se sei pronto a soffrire un po', scoprirai che non torni più a JavaScript puro senza qualche lacrima di nostalgia.
La verità è che TypeScript è un investimento. Le prime settimane sono frustranti: litighi col compilatore, cerchi su Google errori incomprensibili, maledici il giorno in cui hai deciso di provarlo. Poi qualcosa cambia. Inizi a capire i tipi, le interfacce diventano naturali, i generics non ti spaventano più. E soprattutto, inizi a scrivere codice che funziona al primo colpo.
Quando torni a JavaScript puro, ti senti nudo. Dove sono i miei tipi? Come faccio a sapere cosa restituisce questa funzione? Cosa contiene questo oggetto? L'autocomplete diventa stupido, il refactoring diventa pericoloso, i bug si moltiplicano. Ti rendi conto che TypeScript non era una prigione: era un'armatura.
Quindi sì, se sai JavaScript non significa che sai TypeScript. Ma se impari TypeScript, diventi un developer migliore. Più disciplinato, più attento, più consapevole. E quando il tuo collega ti manda un any
in code review, sai esattamente perché ti fa male l'anima.