Le regole qui sotto sono quelle che evitano più bug nel codice TypeScript reale. Ognuna mostra prima la versione comune e poi quella migliore, in codice che puoi eseguire. La prima regola è la più importante: smetti di usare any.
Usa unknown invece di any
any disattiva il controllo dei tipi per un valore e per tutto ciò che ne viene calcolato. Anche unknown accetta qualsiasi valore, ma devi controllarlo prima di usarlo, e questo mette il controllo nel punto in cui i dati entrano nel programma:
I confini sono i punti in cui i tipi smettono di essere garantiti: JSON.parse, le risposte di fetch, localStorage, l'input dei form, le variabili d'ambiente e i messaggi da altri processi. Valida lì con una type guard o una libreria di schemi, e il resto del codice potrà fidarsi dei suoi tipi.
Tieni strict attivo
strict è il predefinito in TypeScript 7; non disattivarlo. Aggiungi i controlli che lascia fuori e che intercettano più bug:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}
noUncheckedIndexedAccess fa includere undefined a arr[i] e record[key], che è ciò che restituiscono per un indice mancante. noImplicitOverride obbliga un metodo di una sottoclasse a dichiarare override, e noFallthroughCasesInSwitch rifiuta un case che prosegue nel successivo.
Lascia lavorare l'inferenza
Annota ciò che TypeScript non può sapere: i parametri delle funzioni e i tipi di ritorno delle funzioni usate da altri moduli. Lascia all'inferenza le variabili locali e i parametri delle callback. Un'annotazione non necessaria non è solo rumore; può rendere un tipo più largo del valore:
Senza il commento @ts-expect-error, setStatus(annotated) è l'errore TS2345. Il const dedotto mantiene il tipo letterale "active", quindi viene accettato. Passa il mouse su una variabile nel tuo editor per vedere cosa è stato dedotto prima di aggiungere un tipo.
Preferisci le union agli enum
Una union di letterali stringa dà autocompletamento e controlli esaustivi senza codice generato. Quando ti serve anche l'elenco dei valori a runtime, ricava il tipo da un array as const:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
function canEdit(role: Role): boolean {
return role !== "viewer";
}
console.log(canEdit("editor")); // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]
canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
Un enum Role { Admin, Viewer } compila in un oggetto con mappatura inversa, e un parametro enum numerico accetta qualsiasi variabile number, anche una che contiene un valore che l'enum non elenca. Inoltre gli enum non possono essere eseguiti con il type stripping di Node (TypeScript enum is not supported in strip-only mode). I pro e i contro sono confrontati nella pagina sugli enum.
Controlla gli oggetti di configurazione con satisfies
Annotare un oggetto con un tipo largo come Record<string, Route> ne controlla i valori ma ne dimentica le chiavi. satisfies controlla la stessa cosa e mantiene il tipo esatto:
Usa un'annotazione quando la variabile deve avere esattamente il tipo dichiarato (un parametro di funzione, un valore che riassegnerai). Usa satisfies per tabelle di lookup, mappe di route, token di tema e altri oggetti costanti.
Modella lo stato con le discriminated union
Un singolo oggetto con campi facoltativi permette stati impossibili: loading: true insieme a un error, o data mancante dopo il successo. Una union di oggetti con un tag in comune permette solo gli stati reali, e ogni ramo vede solo i propri campi:
La riga con never è il controllo di esaustività. Quando qualcuno aggiunge uno stato e dimentica di gestirlo, la build si rompe su quella riga:
L'errore è index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. e nomina il caso non gestito. Aggiungi case "cancelled": e compila.
Evita ! e as
La non-null assertion x! e la type assertion x as T dicono al compilatore di smettere di controllare. Nessuna delle due cambia il valore a runtime, quindi un'asserzione sbagliata diventa un crash più avanti, lontano dalla sua causa:
Sostituisci ! con ?., ??, un return anticipato o un errore lanciato con un messaggio utile. Sostituisci as con una type guard che verifichi davvero il valore. L'unica asserzione sempre sicura è as const, perché rende un tipo solo più stretto e di sola lettura. Una buona configurazione di lint (le regole no-non-null-assertion e no-explicit-any di typescript-eslint) segnala il resto.
Rendi i dati readonly
Marca proprietà e array come readonly quando il codice non deve modificarli. Il compilatore rifiuta allora push, sort e le assegnazioni, e le funzioni restituiscono nuovi valori invece di modificare i loro input:
readonly viene controllato solo in compilazione e solo a un livello di profondità: non congela l'oggetto a runtime. Basta comunque a intercettare la modifica accidentale dello stato condiviso che causa la maggior parte di questi bug.
Domande frequenti
Dovrei usare any in TypeScript?
Quasi mai nel codice applicativo. any disattiva i controlli per il valore e per tutto ciò che ne deriva. Usa unknown per i valori di cui non conosci ancora il tipo, e restringili con dei controlli; tieni any per rare vie di fuga, con un commento che spieghi il perché.
Devo annotare ogni variabile in TypeScript?
No. Lascia che TypeScript deduca le variabili locali e i parametri delle callback. Annota i parametri delle funzioni (non si possono dedurre) e i tipi di ritorno delle funzioni esportate, così una modifica dentro la funzione non può cambiare in silenzio il suo tipo pubblico.
Gli enum di TypeScript sono una cattiva pratica?
Non sono sbagliati, ma molti team li evitano. Un enum genera codice a runtime, non può essere eseguito con il type stripping di Node, e un parametro enum numerico accetta qualsiasi variabile number, qualunque valore contenga. Una union di letterali stringa, o un array as const con un tipo derivato, dà lo stesso autocompletamento e gli stessi controlli senza codice generato.
Quando usare le type assertion con as?
Solo quando sai qualcosa che il compilatore non può sapere, e preferibilmente subito dopo un controllo che lo dimostri. as non cambia nessun valore a runtime, quindi {} as User compila e poi non ha name. Una type guard che verifica il valore è lo strumento più sicuro nella maggior parte dei casi.
Quali impostazioni di tsconfig sono migliori per un nuovo progetto TypeScript?
Tieni strict attivo (il predefinito di TypeScript 7) e aggiungi noUncheckedIndexedAccess. Molti progetti attivano anche noImplicitOverride, noFallthroughCasesInSwitch e verbatimModuleSyntax. La configurazione scritta da tsc --init imposta, tra gli altri, strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes e verbatimModuleSyntax.