Menu

Type narrowing in TypeScript: come funziona la control flow analysis

Il narrowing è il modo in cui TypeScript trasforma un tipo ampio come string | number in uno specifico dentro un if, uno switch o dopo un return anticipato. Tutte le forme di narrowing in un unico posto: typeof, truthiness, uguaglianza, in, instanceof, assegnazioni, type predicate e discriminated union.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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

FormaEsempioRestringe
typeoftypeof x === "string"primitivi e funzioni
Truthinessif (x)toglie null, undefined e i letterali falsy
Uguaglianzax === "a", x == null, x !== undefinedletterali, null, undefined
in"swim" in petunion di oggetti, per proprietà
instanceoferr instanceof TypeErroristanze di classi
Array.isArrayArray.isArray(x)array rispetto a tutto il resto
Assegnazionex = 5al tipo assegnato
Type predicatefunction isUser(x: unknown): x is Userqualsiasi cosa tu possa controllare
Assertion functionfunction assertUser(x: unknown): asserts x is Usertutto ciò che segue la chiamata
Proprietà discriminanteswitch (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.name restringe obj.name (e obj["name"]), ma non obj[key] quando key è una variabile string invece di un letterale, e nemmeno una copia fatta prima del controllo.
  • Callback e riassegnazione. Dentro una callback, una let ristretta mantiene il narrowing solo se non viene assegnata di nuovo dopo la creazione della callback. Una const o un parametro mai riassegnato resta ristretto.
  • Controlli nascosti in funzioni di supporto. Una funzione isString(x: unknown): boolean non dice nulla al compilatore. Dalle un type predicate come tipo di ritorno, x is string, e le sue chiamate restringono il tipo come fa typeof.

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).

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA