Quando utilizziamo un'unione di tipi, una variabile può avere più tipi possibili. Per esempio, sia string che number. In questo caso TypeScript non sa a priori se valore conterrà una stringa o un numero.
Prima di utilizzare operazioni specifiche di uno dei due tipi, possiamo quindi controllare il valore e restringere i possibili tipi. Questo processo viene chiamato Type Narrowing.
TypeScript analizza le condizioni e il flusso del programma per capire quali tipi siano possibili in ogni punto del codice. Questo processo viene chiamato analisi del flusso di controllo.
Consideriamo una variabile che può contenere una stringa oppure un numero:
function stampa(valore: string | number) {
// ...
}Se proviamo a utilizzare direttamente un metodo specifico delle stringhe, TypeScript segnala un errore perché toUpperCase() esiste sulle string, ma non sui number:
function stampa(valore: string | number) {
console.log(valore.toUpperCase());
}Possiamo prima verificare il tipo:
function stampa(valore: string | number) {
if (typeof valore === "string") {
console.log(valore.toUpperCase());
}
}All'interno dell'if, TypeScript sa che valore è sicuramente una string. Il tipo è quindi passato da string | number a string. Questo restringimento del tipo è il Type Narrowing.
Uno dei metodi più comuni per effettuare il Type Narrowing consiste nell'utilizzare l'operatore typeof. L'operatore typeof permette di verificare il tipo di un valore nell'esecuzione.
Ad esempio:
function stampa(valore: string | number) {
if (typeof valore === "string") {
console.log(valore.toUpperCase());
} else {
console.log(valore.toFixed(2));
}
}Nell'esempio, TypeScript analizza la condizione typeof valore === "string" e sa che:
valore è una string. valore è un number. Possiamo quindi utilizzare metodi specifici per ciascun tipo.
L'operatore typeof può verificare i seguenti tipi primitivi:
| Tipo | Descrizione |
string | Valori di testo. |
number | Valori numerici. |
boolean | Valori booleani, true o false. |
bigint | Valori numerici interi grandi. |
symbol | Identificatore univoco. |
undefined | Variabile che non contiene un valore. |
function | Variabile che identifica una funzione. |
object | Variabile che identifica un oggetto o una tupla. |
Nota: abbiamo approfondito meglio questi tipi nella lezione sui tipi primitivi in TypeScript.
Ad esempio:
function analizza(valore: string | number | boolean) {
if (typeof valore === "string") {
console.log(valore.toUpperCase());
} else if (typeof valore === "number") {
console.log(valore.toFixed(2));
} else {
console.log(valore ? "vero" : "falso");
}
}In ogni ramo TypeScript restringe il tipo di valore in base alle condizioni incontrate.
Quando lavoriamo con oggetti che hanno strutture differenti, possiamo utilizzare l'operatore in per verificare se una determinata proprietà esiste.
Consideriamo questi due tipi (definiti come alias di tipi):
type Persona = {
nome: string;
};
type Azienda = {
ragioneSociale: string;
};Possiamo creare un'unione:
type Soggetto = Persona | Azienda;Ora possiamo utilizzare in per distinguere i due casi:
function descrivi(soggetto: Soggetto) {
if ("nome" in soggetto) {
console.log(soggetto.nome);
} else {
console.log(soggetto.ragioneSociale);
}
}La condizione "nome" in soggetto verifica se l'oggetto contiene una proprietà chiamata nome. TypeScript utilizza questa informazione per restringere il tipo.
L'operatore in è particolarmente utile quando gli oggetti di un'unione di tipi hanno proprietà differenti. Ad esempio:
type Cane = {
nome: string;
abbaia: boolean;
};
type Gatto = {
nome: string;
miagola: boolean;
};
type Animale = Cane | Gatto;Possiamo distinguere i due tipi verificando una proprietà caratteristica:
function descrivi(animale: Animale) {
if ("abbaia" in animale) {
console.log("È un cane");
} else {
console.log("È un gatto");
}
}Dopo il controllo, TypeScript sa quali proprietà sono disponibili in ciascuna condizione.
Il Type Narrowing è possibile perché TypeScript analizza il flusso del programma. Prendiamo questo esempio per capire meglio:
function esempio(valore: string | number) {
if (typeof valore === "string") {
console.log(valore.toUpperCase());
return;
}
console.log(valore.toFixed(2));
}TypeScript sa che, dopo il return, il ramo precedente non può più essere eseguito. Di conseguenza, nel punto successivo sa che valore non può più essere una string. Rimane quindi soltanto number.
L'analisi del flusso di controllo permette a TypeScript di tenere conto non soltanto dei tipi dichiarati, ma anche delle condizioni e delle istruzioni che modificano il flusso dell'esecuzione.