TypeScript strict mode: perché attivarlo da subito è una delle scelte migliori che puoi fare
C'è una riga che compare quasi sempre nel `tsconfig.json` di un progetto nuovo, spesso già attiva per default quando si genera il progetto con `create-next-app` o `create vite`: `"strict": true`. E c'è una cosa che ho imparato a fare, ogni volta che entro in una codebase che non ho creato io: vado a controllare se quella riga è ancora lì, oppure se qualcuno l'ha rimossa o commentata. L'ho trovata commentata abbastanza spesso da farmi chiedere il motivo. La risposta che ricevo più frequentemente è: "faceva troppe storie". In questo articolo ti spiego perché quella risposta è, di fatto, l'esatto contrario di un buon motivo per disattivarla.
Cosa fa realmente `"strict": true`
Il primo malinteso da chiarire è che `"strict": true` non è una singola opzione: è uno shorthand che attiva un gruppo di opzioni al suo interno. Nel 2026, con TypeScript 5.x, questo gruppo include:
- `strictNullChecks`
- `strictFunctionTypes`
- `strictBindCallApply`
- `strictPropertyInitialization`
- `noImplicitAny`
- `noImplicitThis`
- `alwaysStrict`
- `useUnknownInCatchVariables`
Ognuna di queste ha un effetto specifico sul comportamento del type checker, ma quella che produce il maggior numero di errori a breve termine, e dunque quella che spinge molti a capitolare, è `noImplicitAny`. Con questa opzione attiva, TypeScript rifiuta di inferire automaticamente `any` per i parametri di una funzione quando non riesce a dedurne il tipo dal contesto. In pratica, se scrivi una funzione e non specifichi il tipo dei parametri, invece di passarci sopra in silenzio, TypeScript ti avverte. E a molte persone questo sembra un ostacolo, proprio quando lo incontrano per la prima volta in un progetto di dimensioni reali.
Perché `any` sembra comodo (e perché smette di esserlo)
Quando inizi a lavorare con TypeScript partendo da una base JavaScript, il tipo `any` sembra una benedizione. Ti permette di fare quello che facevi prima senza dover fermarti a pensare ai tipi. Puoi passare qualsiasi valore a qualsiasi funzione, accedere a qualsiasi proprietà senza che il compilatore dica niente. In pratica, `any` disattiva il type checker per quella variabile specifica.
Il problema è che `any` si diffonde. Una funzione che riceve un parametro di tipo `any` restituisce spesso qualcosa che TypeScript non riesce a tipare correttamente, e quel risultato viene passato a un'altra funzione, che a sua volta restituisce qualcosa di non tipato, e così via. Nel giro di qualche settimana su un progetto attivo, ti ritrovi con zone intere della codebase in cui TypeScript non controlla nulla, dal momento che l'`any` si è propagato silenziosamente. A quel punto hai TypeScript installato, ma non stai usando TypeScript. Stai usando JavaScript con una cerimonia aggiuntiva.
Ho visto questa dinamica prendere forma in più di un progetto, e il punto critico arriva invariabilmente quando si fa un refactoring. Cambi il tipo di ritorno di una funzione, sposti una proprietà, rinomini un campo in un'interfaccia: con `noImplicitAny` attivo e i tipi definiti correttamente, l'IDE ti mostra immediatamente tutti i punti della codebase che dipendono da quella funzione e che ora devono essere aggiornati. Senza i tipi, o con `any` diffuso, non lo scopri fino a quando non si rompe qualcosa in produzione. E scoprirlo in produzione è sempre più costoso che scoprirlo in sviluppo.
Il beneficio che vale la pena di sopportare il costo iniziale
Di tutti i miglioramenti che lo strict mode introduce, quello che ha cambiato più concretamente il mio modo di scrivere codice è `strictNullChecks`. Con questa opzione attiva, TypeScript distingue tra un valore che può essere `null` o `undefined` e uno che non può esserlo. Senza strict mode, `null` e `undefined` sono sottotipi di ogni altro tipo, dunque puoi assegnare `null` a una stringa o a un numero senza che TypeScript ti fermi.
Questo sembra un dettaglio tecnico, ma nella pratica è uno dei bug più frequenti che esistono nel codice JavaScript: il famoso `Cannot read properties of null (reading 'something')` in console. Con `strictNullChecks` attivo, prima di poter accedere a una proprietà su un valore che potrebbe essere `null`, TypeScript ti obbliga a verificare che non lo sia. Non è una formalità, è un modo per rendere esplicita una classe di errori che altrimenti ti arriva solo a runtime.
Un esempio concreto: supponiamo che tu abbia una funzione che cerca un utente nel database e restituisce `User | null`. Senza `strictNullChecks`, puoi accedere a `utente.nome` direttamente senza alcun controllo, e il codice compila senza errori. Se per qualsiasi motivo la funzione restituisce `null` in produzione, hai un crash. Con `strictNullChecks` attivo, TypeScript rifiuta di compilare quella riga fino a quando non aggiungi un controllo esplicito: `if (utente !== null)` oppure usi l'optional chaining `utente?.nome`. Sei tu che decidi come gestire il caso `null`, non il runtime che lo gestisce per te con un errore.
Nei progetti su cui ho lavorato, questa sola opzione ha eliminato un'intera categoria di bug che prima richiedevano debug manuale e spesso emergevano soltanto in scenari di edge case che in sviluppo non si riproducevano facilmente.
`strictFunctionTypes` e il narrowing: perché la struttura dei tipi conta
Un'altra opzione che lo strict mode abilita, e che vale la pena capire anche solo concettualmente, è `strictFunctionTypes`. Quest'ultima cambia il modo in cui TypeScript verifica la compatibilità tra tipi di funzione, passando da un controllo bidirezionale (covariante e controvariante) a uno più rigoroso per i parametri.
In termini pratici, il caso che questa opzione risolve è quello delle funzioni di callback tipate in modo troppo permissivo. Senza `strictFunctionTypes`, TypeScript può accettare una funzione che si aspetta un tipo più specifico al posto di una che ne accetta uno più generico, aprendo la porta a errori a runtime che a prima vista sembrano impossibili guardando solo i tipi. Con l'opzione attiva, questa classe di incompatibilità viene catturata a compile time.
Il concetto di type narrowing, invece, è legato a come TypeScript restringe il tipo di una variabile all'interno di un blocco condizionale. Se dichiari una variabile come `string | null` e poi scrivi `if (valore !== null)`, dentro quel blocco TypeScript sa che `valore` è soltanto `string`. Questo meccanismo diventa molto più utile quando `strictNullChecks` è attivo, dal momento che senza di lui la distinzione tra null e non-null non esiste di fatto nel type system.
Imparare a usare il narrowing in modo consapevole, combinato con lo strict mode, è uno di quei passaggi che separa chi usa TypeScript come JavaScript con i tipi da chi ne sfrutta davvero le capacità.
Come attivarlo in un progetto esistente senza impazzire
Se stai partendo da zero, l'unica cosa giusta da fare è attivare `"strict": true` nel `tsconfig.json` dal primo giorno. Avrai meno errori da affrontare subito, e il progetto crescerà con basi solide fin dall'inizio.
Il caso difficile è quello di un progetto esistente in JavaScript o TypeScript senza strict mode, in cui aggiungere `"strict": true` produce centinaia o migliaia di errori in una volta sola. Qui la tentazione di rimandare è comprensibile, ma c'è un approccio che funziona meglio della resa: la migrazione graduale.
TypeScript permette di attivare le singole opzioni dello strict mode una alla volta. Puoi iniziare aggiungendo soltanto `"strictNullChecks": true`, risolvere quegli errori nel tempo, poi aggiungere `"noImplicitAny": true`, e così via. Non è veloce, ma è un percorso sostenibile anche su codebase grandi, dal momento che ogni passo produce un miglioramento reale senza richiedere di bloccare tutto il resto del lavoro.
Un'altra strategia che ho usato con successo su un progetto ereditato è la combinazione di strict mode globale con commenti selettivi di soppressione per i file non ancora migrati. Attivi `"strict": true`, poi aggiungi `// @ts-nocheck` in cima ai file che non hai ancora migrato, e sistemi un file alla volta. Ogni file migrato viene rimosso dalla lista, e la codebase diventa progressivamente più sicura. L'importante è che il nuovo codice che scrivi ogni giorno rispetti lo strict mode fin dall'inizio, invece di continuare ad accumulare debito tecnico.
In entrambi i casi, l'aspetto importante è fare il lavoro in modo incrementale piuttosto che procrastinarlo indefinitamente. Dal momento che ogni settimana si aggiunge nuovo codice, più aspetti ad attivare strict mode, più diventa difficile farlo in seguito.
`useUnknownInCatchVariables`: l'opzione meno conosciuta che vale la pena capire
C'è un'opzione introdotta in TypeScript 4.4, abilitata da `"strict": true`, che in molti non conoscono perché è relativamente recente: `useUnknownInCatchVariables`. Con questa opzione attiva, la variabile catturata in un blocco `catch` ha tipo `unknown` invece di `any`.
Prima di questa opzione, il codice seguente compilava senza errori:
```typescript try { fetchData(); } catch (error) { console.log(error.message); // nessun errore TypeScript } ```
Il problema è che non c'è nessuna garanzia che `error` abbia una proprietà `message`. Gli errori in JavaScript possono essere qualsiasi cosa: un'istanza di `Error`, una stringa, un numero, un oggetto personalizzato. Con `useUnknownInCatchVariables` attivo, TypeScript ti obbliga a verificare di che tipo è `error` prima di accedere alle sue proprietà:
```typescript try { fetchData(); } catch (error) { if (error instanceof Error) { console.log(error.message); // qui TypeScript sa che è un Error } } ```
Questo cambiamento piccolo nella sintassi ha un impatto concreto sulla qualità della gestione degli errori, che in molti progetti è quest'ultima una delle aree con più bug difficili da tracciare. Un'applicazione che gestisce gli errori in modo esplicito è un'applicazione che si comporta in modo prevedibile anche nei casi di edge.
Il costo reale di non usarlo
Mi capita spesso di sentire che "TypeScript strict fa perdere tempo". È una posizione comprensibile in un certo senso: nella prima settimana su un nuovo progetto, o nelle prime ore di migrazione di una codebase esistente, il numero di errori da risolvere sembra enorme. Ma il paragone corretto non è "quanto tempo ci vuole per risolvere questi errori adesso" contro "quanto tempo ci voleva a ignorarli". Il paragone corretto è "quanto tempo ci vuole adesso" contro "quanto tempo ci vorrà a debuggare questi stessi problemi in produzione, spesso sotto pressione, senza il contesto fresco che hai adesso".
Nella mia esperienza, ogni ora spesa a definire tipi corretti e a risolvere gli errori di strict mode torna indietro con gli interessi nella fase di manutenzione. Ogni volta che mi sono trovato a fare un refactoring significativo su una codebase con strict mode attivo, ho avuto la fiducia di sapere che se tutto compila, quasi certamente funziona. Non sempre, perché i test restano necessari, ma la superficie di errori possibili si riduce in modo MISURABILE.
Ogni volta che me lo sono trovato disattivato, o con `any` diffuso ovunque, ho scoperto le incoerenze a runtime. Non sempre in produzione, ma spesso in momenti inopportuni: durante una demo, alla prima esecuzione in un ambiente diverso, nel codice scritto da un'altra persona che pensava di usare i tipi in un certo modo ma in realtà li usava in un altro.
Dove finisce lo strict mode e dove inizia il resto
Attivare `"strict": true` è il punto di partenza, non il punto di arrivo. Esistono opzioni aggiuntive che lo strict mode non abilita di default, ma che vale la pena valutare in base al progetto: `noUncheckedIndexedAccess`, che rende l'accesso agli array e agli oggetti indicizzati intrinsecamente opzionale, è una di quelle che consiglio di esplorare quando sei già a tuo agio con lo strict mode base. `exactOptionalPropertyTypes`, introdotta in TypeScript 4.4, è un'altra opzione che distingue tra una proprietà opzionale assente e una proprietà opzionale impostata esplicitamente a `undefined`. Entrambe aggiungono ulteriore precisione al type system, seppur richiedano un po' più di attenzione nella gestione dei tipi.
Ma questi sono passi successivi. Il primo, quello che cambia davvero la qualità del codice che scrivi, è soltanto quello di aprire il `tsconfig.json` e assicurarti che `"strict": true` sia lì e che nessuno l'abbia commentata.
Se stai partendo un progetto questa settimana, fallo adesso. Se stai lavorando su una codebase esistente, inizia con `"strictNullChecks": true` e vai avanti un passo alla volta. Non aspettare il momento perfetto, dal momento che quel momento non arriva mai. Il codice che scrivi oggi con i tipi sbagliati è il codice che cercherai di capire tra sei mesi sotto pressione.
Antonio Tufo
Full-Stack Developer & Interaction Designer. Lavoro con startup e PMI italiane.
