Un punto esclamativo dopo un'espressione, value!, è l'operatore di non-null assertion. Toglie null e undefined dal tipo, così un number | undefined si può usare come un number. È una promessa fatta al compilatore, non un controllo: a runtime non succede niente.
Senza il !, tea.toFixed(2) è l'errore TS18048, 'tea' is possibly 'undefined'. Con il !, il codice compila perché hai detto al compilatore che la chiave esiste.
Cosa fa x! a runtime: niente
Il ! viene cancellato dall'output. Il JavaScript compilato per prices.get("coffee")! è semplicemente prices.get("coffee"). Se l'asserzione è sbagliata, l'errore compare più avanti, nel primo punto in cui si usa il valore mancante:
Il programma stampa Cannot read properties of undefined (reading 'toFixed'). Il crash avviene nella riga dopo il !, e nel codice reale può essere molto più lontano: l'undefined può finire salvato in un oggetto ed esplodere in un altro file. È quella distanza a rendere rischioso il !.
Definite assignment: let x!: T
Lo stesso simbolo in una dichiarazione ha un significato affine. TypeScript tiene traccia di se una variabile viene assegnata prima di essere letta, e non riesce a seguire un'assegnazione fatta dentro un'altra funzione:
index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.
let config!: { port: number }; è una definite assignment assertion: "verrà assegnata prima di qualsiasi lettura". Risolve l'errore, con lo stesso rischio di x!: se init() viene saltata, la lettura ottiene undefined. Di solito è meglio ristrutturare, per esempio const config = init(); con init che restituisce l'oggetto.
Proprietà di classe: prop!: T
Con strictPropertyInitialization (parte di strict), ogni proprietà di classe deve essere inizializzata nella sua dichiarazione o nel costruttore. Altrimenti ottieni TS2564: Property 'socket' has no initializer and is not definitely assigned in the constructor. Quando una proprietà viene impostata più tardi da un metodo o da un framework, prop!: T dice al compilatore di accettarla:
Il primo console.log mostra il buco: il tipo dice che socket c'è sempre, ma prima di open() è undefined. Chiamare c.socket.send in quel momento compila e lancia un errore. Se la proprietà può davvero mancare, dichiarala socket?: ... e controllala, oppure crea l'oggetto nel costruttore. Il caso principale in cui prop! è la pratica standard è un framework che riempie una proprietà dopo la costruzione: il @ViewChild(...) child!: ChildDirective di Angular (impostato prima che venga eseguito ngAfterViewInit), o le classi entità degli ORM, le cui colonne vengono riempite dalla libreria quando carica una riga (la documentazione di MikroORM scrive @Property() title!: string).
Alternative più sicure
La maggior parte dei ! si può sostituire con qualcosa che il compilatore controlla, o con un controllo che fallisce in modo evidente nel punto giusto:
Entrambi gli helper restringono il tipo come fa !, ma un'ipotesi sbagliata produce missing HOST sul posto invece di un TypeError da qualche altra parte. assertDefined è una assertion function (asserts value is ...): dopo la chiamata, il compilatore tratta host come una string. Altri schemi nei type guards.
| Invece di | Scrivi | Cosa succede quando il valore manca |
|---|---|---|
user!.name | if (user) { user.name } | il blocco viene saltato |
user!.name | user?.name | undefined |
count! | count ?? 0 | si usa il valore predefinito |
map.get(k)! | must(map.get(k), "k") | un errore chiaro in quella riga |
let x!: T | const x = compute() | non c'è niente che possa andare storto |
Gli altri punti esclamativi
! significa cose diverse a seconda di dove si trova:
| Codice | Significato |
|---|---|
value! (dopo un'espressione) | non-null assertion, solo TypeScript |
let x!: T, prop!: T | definite assignment assertion, solo TypeScript |
!value (prima di un'espressione) | NOT logico, JavaScript puro |
!!value | converte in booleano, JavaScript puro |
a !== b, a != b | disuguaglianza, JavaScript puro |
Solo i primi due vengono cancellati in fase di compilazione. !value e !!value vengono eseguiti a runtime e restituiscono un booleano.
Domande frequenti
Cosa significa un punto esclamativo dopo una variabile in TypeScript?
value! è l'operatore di non-null assertion. Dice al compilatore che value non è null né undefined, quindi il suo tipo perde quei due membri: string | undefined diventa string. Viene rimosso dal JavaScript generato e non aggiunge nessun controllo a runtime, quindi se ti sbagli il programma fallisce più avanti con un TypeError.
Che differenza c'è tra ! e ? in TypeScript?
x! afferma che il valore c'è e ti dà il tipo non nullo, senza controlli. x?.y controlla a runtime: se x è null o undefined si ferma e restituisce undefined. In una dichiarazione, name?: string rende opzionale una proprietà, mentre name!: string dice che una proprietà obbligatoria verrà assegnata in un punto che il compilatore non vede.
Cosa significa let x!: string?
È una definite assignment assertion. Dice al compilatore che la variabile verrà assegnata prima di essere letta, anche se il compilatore non riesce a dimostrarlo (per esempio perché l'assegnazione avviene dentro un'altra funzione). Senza, leggere la variabile è l'errore TS2454, Variable 'x' is used before being assigned.
Come risolvo "has no initializer and is not definitely assigned in the constructor"?
È l'errore TS2564 di strictPropertyInitialization. Dai alla proprietà un valore iniziale, assegnala nel costruttore, rendila opzionale (prop?: T) oppure, se un framework o un metodo di inizializzazione la imposta davvero prima dell'uso, scrivi prop!: T. Il ! è l'ultima opzione perché nessuno controlla la promessa.
L'operatore di non-null assertion è una cattiva pratica?
Non è sbagliato, ma ogni ! è un'affermazione non verificata. Configurazioni di lint come @typescript-eslint/no-non-null-assertion lo segnalano. Preferisci un controllo che restringa il tipo (if (x), x ?? fallback, x?.y) o un helper che lanci un errore chiaro. Tieni ! per i punti in cui il valore è garantito da una logica che il compilatore non riesce a seguire.