Type Narrowing e analisi del flusso di controllo in TypeScript

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.

Cos'è il Type Narrowing

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.

Operatore typeof

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:

  • Nel primo ramo valore è una string.
  • Nel secondo ramo 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.

Operatore in

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.

Analisi del flusso di controllo

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.

Prova!Completa gli spazi vuoti con il testo appropriato.
type Auto = {
    targa: string;
};
type Moto = {
    cilindrata: number;
};

function descrivi(veicolo: Auto | Moto) {
    if ("targa" veicolo) {
        console.log(veicolo.targa);
    }
}

Prova! Scegli l'opzione corretta tra quelle elencate.
// Verifica il tipo della variabile
function verifica(dato: string | boolean) {
    if (____ dato === "string") {
        console.log(dato.toUpperCase());
    }
}