JavaScript è tipizzato dinamicamente, il che è genuinamente utile — puoi iterare velocemente, prototipare liberamente e pubblicare senza un passaggio di build. Ma quella flessibilità ha un costo: un'intera classe di errori emerge solo a runtime. Una funzione si aspetta una stringa, riceve undefined, e la tua app esplode in produzione alle 2 di notte. TypeScript aggiunge un sistema di tipi statici su JavaScript che cattura esattamente quegli errori in fase di compilazione, nel tuo editor, prima che qualcosa venga pubblicato. Questo articolo spiega cosa significa in pratica — con codice reale, compromessi onesti e senza hype.

Cos'è Davvero TypeScript

TypeScript è un superset di JavaScript: ogni file .js valido è anche un file .ts valido. Puoi rinominare un file, aggiungere zero annotazioni di tipo e compila bene. Ciò che TypeScript aggiunge — annotazioni di tipo, interfacce, generici, enum — viene completamente rimosso in fase di compilazione. L'output è JavaScript puro. TypeScript non cambia ciò che viene eseguito; cambia ciò che puoi sapere prima che venga eseguito.

Il compilatore è tsc, installato tramite npm. Lo punti ai tuoi file .ts e genera i file .js che il tuo runtime (Node.js, un browser, Deno) può eseguire. Nessuna sintassi specifica di TypeScript arriva mai in produzione — i tipi esistono solo per lo sviluppatore e la toolchain. Il repository TypeScript su GitHub è esso stesso un grande progetto TypeScript, il che ti dà un'idea di come si scala.

bash
# Compile a single file
npx tsc src/index.ts

# Compile a whole project (uses tsconfig.json)
npx tsc

# Watch mode — recompile on every save
npx tsc --watch

Il Vantaggio Principale: Errori Prima del Runtime

Ecco il problema che TypeScript risolve più visibilmente. Immagina di lavorare con la risposta di un'API dove un campo potrebbe essere una stringa o null. In JavaScript puro, questo fallisce silenziosamente fino alla produzione:

js
// JavaScript — no error until this actually runs with a null value
function formatDisplayName(user) {
  return user.displayName.toUpperCase(); // 💥 TypeError if displayName is null
}

// This looks fine in isolation. The bug only appears when a user
// has no display name set — which might be rare, but it will happen.
const name = formatDisplayName({ displayName: null });

TypeScript cattura questo prima che il codice venga eseguito:

ts
interface User {
  id: number;
  displayName: string | null;
  email: string;
}

function formatDisplayName(user: User): string {
  return user.displayName.toUpperCase();
  // ❌ TypeScript error: Object is possibly 'null'.
  //    Property 'toUpperCase' does not exist on type 'null'.
}

// Fix: handle the null case explicitly
function formatDisplayName(user: User): string {
  if (user.displayName === null) {
    return user.email; // fall back to email
  }
  return user.displayName.toUpperCase(); // TypeScript now knows this is safe
}

Questo è il momento di rivelazione per la maggior parte degli sviluppatori. Il messaggio di errore ti dice esattamente cosa c'è di sbagliato e dove — nel tuo editor, prima di eseguire una singola riga. Con la modalità strict abilitata, TypeScript è particolarmente aggressivo nel catturare i problemi di null e undefined attraverso la sua funzionalità strictNullChecks, che è attiva per impostazione predefinita in modalità strict.

Il pattern da interiorizzare: gli errori TypeScript sono a tempo di compilazione, non a runtime. Un errore da tsc significa che il tuo codice ha un'inconsistenza di tipo — TypeScript può provare che fallirà (o potrebbe fallire) senza eseguirlo. Correggi l'errore di tipo e hai eliminato completamente quella classe di bug.

Il Sistema di Tipi di TypeScript in Breve

Non devi annotare tutto. TypeScript inferisce i tipi da assegnazioni, valori di ritorno e chiamate di funzione — spesso scrivi JavaScript normale e ottieni il controllo dei tipi gratuitamente. Ma conoscere la sintassi delle annotazioni aiuta quando l'inferenza non è sufficiente.

ts
// Primitives
const productName: string  = 'Wireless Keyboard';
const price:       number  = 79.99;
const inStock:     boolean = true;

// TypeScript infers these without annotations — same effect
const productName = 'Wireless Keyboard'; // inferred: string
const price       = 79.99;               // inferred: number

// Arrays
const tags:     string[]  = ['electronics', 'peripherals'];
const ratings:  number[]  = [4.5, 4.8, 4.2];

// Objects with interfaces
interface Product {
  id:          number;
  name:        string;
  price:       number;
  category:    string;
  inStock:     boolean;
  description: string | null; // union type — string or null
}

// Union types — a value that could be one of several types
type Status = 'active' | 'inactive' | 'pending'; // string literal union
type ID     = string | number;                    // string or number

// Function with typed parameters and return type
function formatProductCard(product: Product): string {
  const stockLabel = product.inStock ? 'In Stock' : 'Out of Stock';
  const desc       = product.description ?? 'No description available';
  return `${product.name} — $${product.price.toFixed(2)} (${stockLabel})
${desc}`;
}

// Generic function — works with any type, preserves it
function firstOrDefault<T>(items: T[], fallback: T): T {
  return items.length > 0 ? items[0] : fallback;
}

const first = firstOrDefault(['a', 'b', 'c'], 'z'); // inferred: string
const num   = firstOrDefault([1, 2, 3],       0);   // inferred: number

Il manuale di TypeScript approfondisce il sistema dei tipi — è davvero ben scritto e vale la pena leggerlo una volta acquisite le basi.

Configurare TypeScript in un Progetto

Aggiungere TypeScript a un progetto richiede circa cinque minuti. Installi il compilatore come dipendenza di sviluppo, generi un file di configurazione e inizi a rinominare i file .js in .ts al ritmo che ha senso.

bash
# Install TypeScript as a dev dependency
npm install -D typescript

# Generate tsconfig.json with sensible defaults
npx tsc --init

Il tsconfig.json generato ha decine di opzioni, la maggior parte commentate. Le principali da conoscere:

json
{
  "compilerOptions": {
    "target": "ES2020",       // what JavaScript version to output
    "module": "commonjs",     // module system (commonjs for Node, ESNext for bundlers)
    "outDir": "./dist",       // where compiled .js files go
    "rootDir": "./src",       // where your .ts source files live
    "strict": true,           // enables all strict type checks — highly recommended
    "esModuleInterop": true,  // smoother interop with CommonJS modules
    "skipLibCheck": true      // skip type checks on .d.ts files in node_modules
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Abilita sempre strict: true su un nuovo progetto. Raggruppa diversi controlli tra cui strictNullChecks, noImplicitAny e strictFunctionTypes. Su una codebase esistente potresti dover abilitarli gradualmente — ma per qualsiasi cosa greenfield, inizia strict.

TypeScript vs JavaScript — I Veri Compromessi

TypeScript ha molti vantaggi genuini, ma anche costi reali. Ecco una valutazione onesta:

  • Autocompletamento che funziona davvero. Il tuo editor conosce la forma di ogni oggetto, quindi può suggerire i nomi di proprietà e le firme dei metodi corretti invece di indovinare. Questo da solo fa risparmiare tempo ogni singolo giorno.
  • Bug catturati prima che raggiungano la produzione. Dereferenziazioni null, tipi di argomento errati, proprietà richieste mancanti — TypeScript le evidenzia al momento della scrittura, non alle 3 di notte in un canale di incidente.
  • Refactoring più sicuro. Rinomina un campo su un'interfaccia e TypeScript segnala immediatamente ogni sito di chiamata che deve essere aggiornato. In una grande codebase JavaScript, quel tipo di cambiamento è terrificante.
  • Il codice si autodocumenta. Una firma di funzione come sendEmail(to: string, subject: string, body: string, options?: EmailOptions): Promise<SendResult> ti dice tutto ciò che devi sapere senza leggere l'implementazione.
  • Migliore per i team. Quando più sviluppatori condividono una codebase, i tipi formano un contratto tra i moduli. Puoi fare il refactoring del tuo codice senza rompere quello del tuo collega.

E i costi onesti:

  • Aggiunge un passaggio di build. Per un piccolo script o un prototipo rapido, eseguire tsc prima di poter testare è attrito reale. JavaScript vanilla viene eseguito immediatamente.
  • La configurazione iniziale richiede tempo. Configurare tsconfig.json, aggiungere definizioni di tipo per librerie di terze parti (@types/express, ecc.) e configurare correttamente il tuo editor richiede alcune ore di lavoro.
  • any mina tutto. TypeScript ha una via d'uscita — puoi tipizzare qualsiasi cosa come any, il che disabilita il controllo dei tipi per quel valore. L'uso eccessivo di any significa che ottieni l'attrito di TypeScript senza la sicurezza. È una stampella, non una soluzione.
  • I tipi di librerie di terze parti possono essere in ritardo. Non tutti i pacchetti npm includono definizioni TypeScript. Alcuni si affidano a pacchetti @types/* mantenuti dalla community che potrebbero essere incompleti o non aggiornati.
Quando il JS vanilla va bene: un piccolo script CLI, una migrazione dati una-tantum, un prototipo del weekend che butterai via. Il valore di TypeScript si compone con la dimensione del progetto e del team. Uno script da 200 righe usato da un solo sviluppatore non ha bisogno di tipi. Una codebase da 50.000 righe condivisa da otto sviluppatori assolutamente sì.

Dove TypeScript Brilla Davvero

TypeScript paga i dividendi maggiori in tre situazioni: codebase grandi, librerie condivise e qualsiasi cosa che verrà rielaborata nel tempo. Il sistema dei tipi funge da documentazione vivente che è sempre aggiornata — a differenza di commenti o file README.

Un esempio pratico: la tua app chiama un'API REST che restituisce dati utente. Senza TypeScript, stai fidandoti che l'API restituisca quello che ti aspetti. Con TypeScript, modelli la risposta e ottieni feedback immediato se qualcosa non corrisponde:

ts
interface ApiUser {
  id:        number;
  username:  string;
  email:     string;
  avatarUrl: string | null;
  createdAt: string; // ISO 8601 date string
  role:      'admin' | 'editor' | 'viewer';
}

interface ApiResponse<T> {
  data:    T;
  total:   number;
  page:    number;
  perPage: number;
}

async function fetchUsers(page: number): Promise<ApiResponse<ApiUser[]>> {
  const response = await fetch(`/api/users?page=${page}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch users: ${response.status}`);
  }
  return response.json() as Promise<ApiResponse<ApiUser[]>>;
}

// Now every consumer of this function knows exactly what they'll get
const result = await fetchUsers(1);
result.data.forEach(user => {
  // TypeScript knows user.role is 'admin' | 'editor' | 'viewer'
  // It will error if you try to access a property that doesn't exist
  console.log(`${user.username} (${user.role})`);
});

Quando hai bisogno di disattivare temporaneamente il controllo dei tipi, usa unknown invece di any. La differenza: any ignora silenziosamente tutti i controlli, mentre unknown ti costringe a restringere il tipo prima di usare il valore — ottieni la via d'uscita senza perdere completamente la sicurezza.

ts
// ❌ any — TypeScript trusts you completely, no checks
function processData(data: any) {
  data.nonExistentMethod(); // no error — TypeScript looks away
}

// ✅ unknown — you have to prove what it is before using it
function processData(data: unknown) {
  if (typeof data === 'string') {
    console.log(data.toUpperCase()); // safe — TypeScript knows it's a string here
  } else if (Array.isArray(data)) {
    console.log(data.length);        // safe — TypeScript knows it's an array
  }
}

Il TypeScript Playground è il modo più veloce per sperimentare questi pattern. Incolla il codice, guarda l'output JavaScript compilato e gli eventuali errori di tipo in tempo reale — senza installazione necessaria.

Conclusione

TypeScript non è magia e non è adatto a ogni progetto. Ma per qualsiasi codebase JavaScript che stia crescendo, che venga mantenuta da più di una persona, o che verrà rielaborata — cambia genuinamente l'esperienza di sviluppo. Il sistema dei tipi cattura un'intera categoria di bug a runtime prima che vengano pubblicati, rende l'autocompletamento davvero utile e trasforma il refactoring da un processo manuale rischioso a qualcosa che la toolchain gestisce.

Inizia dalla documentazione ufficiale di TypeScript — è ben strutturata e adatta ai principianti. Il TypeScript Playground è ottimo per sperimentare senza nessuna configurazione. La voce del glossario TypeScript di MDN è una buona panoramica in una pagina se vuoi una seconda prospettiva. E se stai lavorando con dati JSON in TypeScript, lo strumento JSON to TypeScript su questo sito genera interfacce TypeScript direttamente da qualsiasi payload JSON — utile quando hai bisogno di modellare una risposta API rapidamente senza scrivere i tipi a mano.