Web Design and Engineering

TypeScript strict mode: gli 8 setting che non disattiviamo mai

Il flag strict: true di TypeScript attiva 8 controlli separati. La maggior parte dei codebase tiene strict acceso alla radice e ne disattiva in silenzio uno o due dentro un file problematico. Ecco gli 8 controlli, cosa intercettano in produzione, e i tre flag fuori dal bundle che attiviamo lo stesso su ogni progetto.

25 maggio 20268 min di lettura
a computer screen with a bunch of code on it

TypeScript strict mode è un singolo flag dentro tsconfig.json che attiva otto controlli separati del compilatore tutti insieme. Gli otto sono strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables e alwaysStrict. La reference TSConfig di Microsoft li elenca dentro la famiglia strict e il team Microsoft ha una proposta aperta (issue 62333) per rendere strict il default in una futura release maggiore.

La maggior parte dei codebase parte con strict: true alla radice e una manciata di file o moduli che in silenzio rinunciano a uno o due di questi controlli. Le rinunce quasi mai tornano indietro. Questo articolo è l'elenco degli otto, con il fallimento che ognuno previene e il prezzo che abbiamo pagato per tenerli accesi. Nessuno è gratis. Nessuno ci è mai costato più del bug che ha intercettato.

Perché il bundle conta

Il flag strict è l'unico contratto stabile. Se un team disattiva uno degli otto, ogni lettore del codice deve ora ricordare quale è stato spento. Ogni dipendenza tipizzata contro strict acquisisce un buco in più. Ogni sviluppatore junior che entra nel team riscopre lo stesso inghippo. Il bundle non è una checklist; è una dichiarazione pubblica su come un codebase ragiona sui tipi. Una volta che un flag è spento, gli altri seguono lentamente.

Gli 8 controlli dentro strict

1. strictNullChecks

Cosa intercetta. null e undefined come tipi distinti da ogni altro tipo. A flag spento, una funzione che ritorna User ritorna implicitamente User | null | undefined, e ogni dereferenza è insicura.

Il costo. Ogni valore da una risposta API, ogni prop opzionale, ogni return di Array.prototype.find() richiede un narrowing.

Perché lo lasciamo acceso. Ogni codebase che abbiamo ereditato con strictNullChecks: false spedisce un bug Cannot read properties of undefined in produzione almeno una volta al trimestre. Il costo runtime di uno di questi crash supera il costo dev di un if di parecchi ordini di grandezza. È la feature di sistema dei tipi più importante in TypeScript, e il consiglio di migrazione della community è costante: attivalo per ultimo perché produce il maggior numero di errori, e non spegnerlo mai più.

2. noImplicitAny

Cosa intercetta. Parametri e valori di ritorno senza annotazione e non inferibili. A flag spento, quelle posizioni diventano in silenzio any.

Il costo. Annotazioni in più sugli helper veloci, soprattutto durante la migrazione da JavaScript.

Perché lo lasciamo acceso. any è un virus. Un solo any in un helper profondo cancella i tipi su tutti i chiamanti, spesso senza un avviso. Ogni team che ho visto disattivare noImplicitAny "per la migrazione" un anno dopo aveva la maggior parte del codice nuovo non tipizzato. Il flag è l'unico meccanismo che obbliga il team a una scelta cosciente quando un tipo è sconosciuto, invece di scivolare in any per inerzia.

3. strictFunctionTypes

Cosa intercetta. Violazioni di controvarianza sui parametri di funzione. Una funzione che riceve un Dog non può essere assegnata a una variabile che si aspetta una funzione che riceve un qualsiasi Animal, perché il tipo più largo la chiamerebbe con valori che la funzione più stretta non sa gestire.

Il costo. Attrito occasionale con tipi di libreria scritti prima dell'esistenza di questo flag. Il flag inoltre non si applica ai parametri in sintassi metodo, quindi i casi più rumorosi restano silenziosi.

Perché lo lasciamo acceso. I bug di callback sono silenziosi a runtime. Un handler che si aspetta il sottotipo sbagliato viene chiamato con valori che non sa gestire e o crasha in profondità nel render o esegue in silenzio la cosa sbagliata. Nel nostro codice non abbiamo mai avuto un errore strictFunctionTypes che si rivelasse un falso positivo.

4. strictBindCallApply

Cosa intercetta. Argomenti sbagliati passati a .bind(), .call() o .apply().

Il costo. Praticamente zero nei codebase moderni, perché .bind() nessuno lo scrive più.

Perché lo lasciamo acceso. Il flag è gratis nel codice che usa arrow function, metodi di classe e hook React. Spegnerlo conterebbe solo per codice legacy che andrebbe modernizzato comunque. Non c'è alcun vantaggio nel disattivarlo.

5. strictPropertyInitialization

Cosa intercetta. Campi di classe dichiarati con un tipo non-undefined ma mai assegnati nel costruttore. Senza questo controllo, il campo è in silenzio undefined alla prima lettura.

Il costo. I campi vanno inizializzati nel costruttore, marcati con l'operatore di assegnamento definito (!), oppure il tipo deve includere undefined.

Perché lo lasciamo acceso. L'alternativa sono campi che sembrano inizializzati nel tipo ma non lo sono a runtime, e che spuntano come bug nel setup dei test e nei container DI. L'operatore ! è la via di fuga per i casi in cui un framework garantisce l'inizializzazione, e l'obbligo di scriverlo rende visibile l'assunzione. Il controllo si ripaga in ogni progetto che usa controller a classe, entità ORM o oggetti di servizio.

6. noImplicitThis

Cosa intercetta. Espressioni this il cui tipo il compilatore non riesce a inferire, tipicamente nei callback passati a forEach o in funzioni stand-alone usate come event handler.

Il costo. Zero nel codice React e Node moderno che usa arrow function e classi ES. Reale nel codice legacy class-heavy, che è poi proprio dove intercetta più bug.

Perché lo lasciamo acceso. Un this legato male è una delle famiglie di bug più antiche di JavaScript. Se TypeScript la intercetta gratis, non c'è motivo di rinunciare.

7. useUnknownInCatchVariables

Cosa intercetta. L'err dentro un blocco catch (err). Con questo flag err è tipizzato come unknown invece che any, quindi non lo si può dereferenziare senza prima fare narrowing.

Il costo. Una riga if (err instanceof Error) per blocco catch.

Perché lo lasciamo acceso. any dentro un catch è uno dei modi più comuni in cui any si infiltra nel resto del codice. err.message finisce in una stringa UI, poi in un payload di log, poi in un breadcrumb Sentry, e a quel punto any si è diffuso su quattro file. Fare narrowing una volta al confine del catch tiene onesta tutta la superficie a valle.

8. alwaysStrict

Cosa intercetta. Garantisce che ogni file parsato sia in strict mode e emette "use strict" nel JavaScript generato.

Il costo. Zero in qualsiasi codice scritto in questo decennio.

Perché lo lasciamo acceso. È già acceso di default in qualsiasi codice a forma di modulo (ESM, moduli CommonJS, classi). Il flag conta soprattutto per gli script e per i blocchi inline. Spento è un odore che qualcuno sta spedendo script semplici, che è un problema a parte.

Tre flag fuori da strict che attiviamo lo stesso

Il bundle strict copre il pavimento. Tre flag stanno fuori e hanno un impatto reale sul conteggio bug in produzione. Li attiviamo dal giorno uno su ogni progetto nuovo.

noUncheckedIndexedAccess

Obbliga obj[key] e arr[i] a essere tipizzati come T | undefined. Il comportamento di default assume che l'accesso vada sempre a buon fine, cosa che è falsa per qualsiasi record o array la cui forma è dinamica. La doc ufficiale lo dichiara in la pagina noUncheckedIndexedAccess, e l'issue 49169 sul repo TypeScript è una discussione di lungo corso sul fatto se vada incluso in strict di default. Dal nostro lavoro: è il flag che aggiunge la pressione di type-check più alta di ogni altro singolo setting, ed elimina la classe di crash runtime più frequente che vediamo ancora nel codice con solo strict: true.

exactOptionalPropertyTypes

Separa "la proprietà potrebbe non essere impostata" da "la proprietà è impostata a undefined". La maggior parte dei team le tratta come la stessa cosa. Non lo sono, soprattutto con prop React passate attraverso wrapper e con stato di form dove un campo svuotato dall'utente produce una stringa vuota mentre un campo mai toccato produce una chiave mancante. Il flag impone la distinzione a livello di tipi e previene la fusione silenziosa che rompe la persistenza dei form.

noPropertyAccessFromIndexSignature

Obbliga obj["key"] per l'accesso dinamico e obj.key per le proprietà dichiarate. L'attrito è reale, ma il flag previene il fall-through silenzioso in cui un refuso in obj.foo ritorna undefined perché il tipo ha un index signature e il refuso non corrisponde a nessuna proprietà dichiarata.

Come migriamo un codebase legacy a strict

La migrazione ha sempre la stessa forma. Si accende strict alla radice in un branch. Si guarda il numero di errori. Si aggiunge un // @ts-nocheck in cima a ogni file che esplode, si registra il conteggio, e si rimuovono quei commenti un file per pull request. Non si abbassa strict per tenere verde la CI; gli errori soppressi oggi sono gli errori che un collega debugga alle 2 di notte il mese prossimo. Se la cremagliera si blocca, si esegue tsc --noEmit -p tsconfig.strict.json solo sui file modificati, così il codice nuovo paga il prezzo pieno mentre il codice vecchio viene ancora ripulito.

L'eccezione che abbiamo fatto, una volta, è stata su strictPropertyInitialization in un codebase NestJS dove il container DI garantisce l'inizializzazione dei campi dopo la costruzione. Anche lì, la risposta giusta è stata l'operatore ! sui campi specifici, non disattivare il flag su tutto il progetto.

Il riassunto in una riga

Ogni flag del bundle strict previene una classe di bug che abbiamo visto in produzione più di una volta. Tre flag fuori dal bundle prevengono i bug più comuni che sopravvivono a strict: true. Nessuno degli undici ci è mai costato più del bug che ha intercettato. Se un team vuole disattivarne uno, la domanda giusta è quale bug preferisce spedire.

Foto di Chris Ried su Unsplash

Domande frequenti

What are the 8 flags that TypeScript strict mode enables?
The strict family contains eight flags: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. Setting strict: true in tsconfig.json turns on all eight at once. Each one targets a specific class of bug, so disabling any single flag while leaving strict: true at the root creates a hidden hole the rest of the codebase no longer guards against.
Should noUncheckedIndexedAccess be enabled by default in TypeScript?
We think yes for any new project, even though it is not inside the strict bundle. The default behaviour types obj[key] and arr[i] as if the access always succeeds, which is wrong for any record or array with a dynamic shape. Issue 49169 in the TypeScript repository tracks the long-running discussion about folding it into strict. The cost is real: it produces the most new type errors of any single flag in our experience. The benefit is that it removes the most common runtime crash class that still survives in code with strict: true alone, particularly around API responses, Record lookups, and array indexing.
Can we enable TypeScript strict mode incrementally on an existing project?
Yes, and the order matters. Turn on strict at the root in a branch, then add // @ts-nocheck at the top of any file that produces too many errors to fix in one pass. Strip those comments one file per pull request, prioritising files closest to the user (API handlers, form code, payment flows). Inside the strict bundle, leave strictNullChecks for last: it produces the largest single error count and the highest cleanup effort. Do not lower strict to make CI green. Suppression at the file level is reversible; suppression at the config level rarely is.
Does TypeScript strict mode slow down the compiler?
Not in any way that matters. The strict flags change how types are checked, not how much work the type-checker does on each file. Build time and tsc --noEmit time are essentially identical with strict on or off. The real cost is in developer time spent fixing the errors strict surfaces, which is a one-time cost on a clean migration and an ongoing tax on new code that pays itself back the first time the flag prevents a production bug.

Studio

Inizia un progetto.

Un partner unico per il prodotto digitale che devi costruire. Produzione più veloce, tecnologie moderne, costi ridotti. Un team, una fattura.