Il type narrowing è TypeScript che ricava un tipo più specifico per un valore in un certo punto del codice, in base ai controlli che il codice ha già eseguito. Un parametro string | number diventa string dentro if (typeof x === "string") e number nell'else.
Chiamare value.toFixed(2) prima del controllo sarebbe un errore di compilazione, perché toFixed non esiste su string. Il controllo è normale JavaScript e viene eseguito a runtime; il narrowing è il compilatore che lo legge e adegua il tipo. Non viene generato nulla in più.
Control flow analysis
TypeScript segue ogni percorso all'interno di una funzione: if/else, return e throw anticipati, switch, cicli e gli operatori di cortocircuito &&, ||, ?? e ?:. In ogni punto, il tipo di una variabile è ciò che lì è ancora possibile.
Lo stile con return anticipati ("guard clause") è il modo più leggibile per restringere i tipi: gestisci prima i casi particolari e il resto della funzione lavora con il tipo pulito.
Tutti i modi per restringere un tipo
| Forma | Esempio | Restringe |
|---|---|---|
| typeof | typeof x === "string" | primitivi e funzioni |
| Truthiness | if (x) | toglie null, undefined e i letterali falsy |
| Uguaglianza | x === "a", x == null, x !== undefined | letterali, null, undefined |
in | "swim" in pet | union di oggetti, per proprietà |
| instanceof | err instanceof TypeError | istanze di classi |
Array.isArray | Array.isArray(x) | array rispetto a tutto il resto |
| Assegnazione | x = 5 | al tipo assegnato |
| Type predicate | function isUser(x: unknown): x is User | qualsiasi cosa tu possa controllare |
| Assertion function | function assertUser(x: unknown): asserts x is User | tutto ciò che segue la chiamata |
| Proprietà discriminante | switch (shape.kind) | union con etichetta |
Le ultime tre sono trattate nelle pagine sui type guard e sulle discriminated union. Le altre sono qui sotto.
Truthiness narrowing
if (x) toglie null e undefined (e i tipi letterali false, 0, ""). È breve, e ha una trappola classica: 0 e "" sono falsy, quindi valori validi vengono trattati come mancanti.
Per numeri e stringhe, confronta in modo esplicito con undefined o null (oppure usa ??). La truthiness va bene per oggetti, array e funzioni, che non sono mai falsy.
Narrowing per uguaglianza
===, !==, == e != restringono entrambi i lati. Confrontare con un letterale restringe a quel letterale; == null (uguaglianza non stretta) corrisponde sia a null sia a undefined in un solo controllo, ed è l'unico caso in cui l'uguaglianza non stretta è idiomatica.
Confrontare due variabili restringe entrambe a ciò che potrebbero avere in comune: se a: string | number e b: string | boolean superano a === b, dentro l'if sono entrambe string.
L'operatore in
"key" in obj restringe una union di tipi oggetto ai membri che hanno (o potrebbero avere) quella proprietà.
in funziona anche su unknown quando sai che è un oggetto: dopo typeof v === "object" && v !== null && "id" in v, TypeScript sa che v ha una proprietà id di tipo unknown. Per le union che progetti tu, una proprietà etichetta condivisa (kind: "fish") è più chiara che andare a cercare i metodi: questo pattern si chiama discriminated union.
Narrowing per assegnazione
Una variabile ha un tipo dichiarato e un tipo ristretto che segue le sue assegnazioni. Assegnare un valore la restringe al tipo di quel valore, entro i limiti del tipo dichiarato.
Quando il narrowing si perde
Il narrowing è locale e prudente. Alcune situazioni lo azzerano:
- Un'espressione diversa. Controllare
obj.namerestringeobj.name(eobj["name"]), ma nonobj[key]quandokeyè una variabilestringinvece di un letterale, e nemmeno una copia fatta prima del controllo. - Callback e riassegnazione. Dentro una callback, una
letristretta mantiene il narrowing solo se non viene assegnata di nuovo dopo la creazione della callback. Unaconsto un parametro mai riassegnato resta ristretto. - Controlli nascosti in funzioni di supporto. Una funzione
isString(x: unknown): booleannon dice nulla al compilatore. Dalle un type predicate come tipo di ritorno,x is string, e le sue chiamate restringono il tipo come fatypeof.
Il compilatore segnala index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. La callback potrebbe essere eseguita più tardi, dopo x = undefined. Elimina quell'ultima assegnazione (o copia il valore in una const dentro l'if) e il codice compila e stampa 5 due volte.
Una funzione di supporto che restituisce boolean si sistema dichiarando ciò che dimostra. È proprio questo un type guard:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Da TypeScript 5.5 il compilatore deduce questi predicati per le arrow function semplici, ed è per questo che list.filter((x) => x !== undefined) ora restituisce un array senza undefined.
Domande frequenti
Cos'è il type narrowing in TypeScript?
Il narrowing è TypeScript che rende più preciso il tipo di una variabile dentro un blocco, in base a un controllo eseguito dal codice. Dopo if (typeof x === "string"), un string | number è solo string dentro l'if e solo number nell'else. Il compilatore segue if, else, return, switch, &&, || e ?: per ricavare il tipo in ogni punto: questo si chiama control flow analysis.
Perché TypeScript non restringe il mio tipo?
Le cause più comuni: il controllo riguarda un'espressione diversa da quella che usi (controlli obj.a, usi obj[key] con una key di tipo string); il valore è una let riassegnata dopo la creazione di una callback, quindi la callback perde il narrowing; oppure il controllo è nascosto in una funzione di supporto che restituisce un semplice boolean invece di un type predicate x is T.
Il type narrowing funziona a runtime?
I controlli sì: typeof, instanceof, in e === sono normale JavaScript che viene eseguito. Il narrowing in sé esiste solo in fase di compilazione. TypeScript legge i tuoi controlli a runtime e adegua di conseguenza i tipi statici, senza aggiungere nulla al JavaScript generato.
Come restringo un tipo unknown in TypeScript?
Con gli stessi controlli: typeof value === "string", Array.isArray(value), value instanceof Date, oppure per gli oggetti typeof value === "object" && value !== null && "id" in value. Per i controlli riutilizzabili scrivi una funzione type guard con tipo di ritorno value is T.
Come tolgo undefined da un array in TypeScript?
items.filter((x) => x !== undefined) restituisce T[] senza undefined da TypeScript 5.5, che deduce la callback come type predicate. Nelle versioni precedenti scrivi tu il predicato: items.filter((x): x is T => x !== undefined).