Variabili e valori
Variabili e valori
Immagina di etichettare gli scatoloni durante un trasloco. Su uno scrivi "LIBRI" con un pennarello indelebile. Da quel momento, quello scatolone è per i libri: se qualcuno, a metà trasloco, ci infila dentro un paio di scarpe, il giorno dopo farai fatica a ritrovarle, perché non è lì che le cercheresti. L'etichetta non impedisce fisicamente di mettere altro dentro alla scatola, ma crea un'aspettativa chiara su cosa dovrebbe contenere.
In TypeScript, quando dichiari una variabile, succede qualcosa di molto simile: la variabile "nasce" con un tipo, che resta associato ad essa per tutta la sua vita, anche se tecnicamente in JavaScript puro potresti assegnarci qualsiasi cosa. Vediamo cosa significa in pratica.
Dichiarazione di variabili e inferenza
Dal 2015, il modo convenzionale di dichiarare variabili in JavaScript è con let e const:
let let temperatura: numbertemperatura = 19;
TypeScript è in grado di inferire che temperatura è di tipo number, semplicemente osservando che la stiamo inizializzando con un valore nel momento stesso in cui la dichiariamo. Non hai scritto : number da nessuna parte, ma il compilatore lo ha capito da solo — passaci sopra con il mouse nel tuo editor (o nel blocco qui sopra) e vedrai il tooltip con il tipo dedotto.
Se proviamo ad assegnare a temperatura un valore incompatibile con number, otteniamo un errore:
let let temperatura: numbertemperatura = 6;
temperatura = "calda";In TypeScript, le variabili "nascono" con il loro tipo. Anche se esistono modi per renderlo più specifico in certi rami del codice (li vedremo più avanti), non c'è modo di cambiare il tipo di temperatura da number a string senza dire esplicitamente a TypeScript di ignorare tutte le informazioni di tipo su questa variabile.
Proviamo la stessa cosa con const:
const const umidita: 79umidita = 79;
Qui il tipo inferito non è number, ma il valore letterale 79. TypeScript riesce a fare un'assunzione più specifica, perché:
- Le variabili dichiarate con
constnon possono essere riassegnate. - Il valore iniziale assegnato a
umiditaè un numero, che è un tipo di valore immutabile.
Di conseguenza, umidita sarà sempre 79 in questo programma.
Tipi letterali
Tipi come 79 si chiamano tipi letterali — puoi pensarli come "è ammesso solo il valore 79".
:::tip Tema ricorrente: inferenza non invasiva
C'è un'idea che vedrai tornare più e più volte lavorando con TypeScript: l'inferenza non è mai così specifica da intralciare il comportamento più comune. Per esempio, la dichiarazione let temperatura = 19 avrebbe potuto far assumere a TypeScript che il tipo fosse 19, ma questo avrebbe reso impossibile riassegnare in seguito temperatura a 7 o 8 — comportamento perfettamente lecito per una variabile dichiarata con let.
:::
Un tipo come insieme di valori ammessi
Spesso è utile pensare a un tipo come a un insieme di valori ammessi. Useremo una notazione comune per descrivere questi insiemi, che assomiglia a questa:
{ 1, 2, 3 } // "1 oppure 2 oppure 3"Torniamo ai nostri esempi di prima:
let let temperatura: numbertemperatura = 19;
const const umidita: 79umidita = 79;
Il tipo number di temperatura rappresenta l'insieme { tutti i numeri possibili }. Puoi assegnare un nuovo numero a temperatura e TypeScript sarà perfettamente d'accordo:
let let temperatura: numbertemperatura = 19;
let temperatura: numbertemperatura = 23; // nessun problemaIl tipo 79 di umidita rappresenta invece l'insieme { 79 }, ovvero "qualsiasi valore, a patto che sia 79".
Possiamo creare una situazione interessante forzando una dichiarazione let a comportarsi, per l'inferenza di tipo, come se fosse una const:
let let temperatura2: numbertemperatura2 = 19;
let let umidita2: 79umidita2 = 79 as type const = 79const;
Nota che abbiamo gli stessi tipi di prima — l'unica cosa che cambia è che umidita2 resta riassegnabile, perché è dichiarata con let. Proviamo qualche assegnazione:
let let temperatura3: numbertemperatura3 = 19;
let let umidita3: 79umidita3 = 79 as type const = 79const;
let temperatura3: numbertemperatura3 = 23; // OK, come prima
let temperatura3: numbertemperatura3 = let umidita3: 79umidita3; // OK
umidita3 = let temperatura3: numbertemperatura3; // non compatibile: number non rientra in 79
let umidita3: 79umidita3 = 79; // OK
umidita3 = 78; // non compatibile: 78 non rientra in 79Ognuna di queste assegnazioni x = y richiede di stabilire una equivalenza di tipo, ovvero rispondere alla domanda: "il tipo di y rientra nel tipo di x?".
Descriviamo cosa sta succedendo usando gli insiemi:
let let temp2: numbertemp2 = 19; // il tipo di temp2 è { tutti i numeri }
let let umid2: 79umid2 = 79 as type const = 79const; // il tipo di umid2 è { 79 }
// Ogni membro di { 23 } è anche in { tutti i numeri }?
let temp2: numbertemp2 = 23;
// Ogni membro di { 79 } è anche in { tutti i numeri }?
let temp2: numbertemp2 = let umid2: 79umid2;
// Ogni membro di { tutti i numeri } è anche in { 79 }?
umid2 = let temp2: numbertemp2;
// Ogni membro di { 79 } è anche in { 79 }?
let umid2: 79umid2 = 79;
// Ogni membro di { 78 } è anche in { 79 }?
umid2 = 78;Quello che vediamo è che il tipo 79 è equivalente (compatibile) con number, ma non vale il contrario. { 79 } è un sottoinsieme di { tutti i numeri }, e quindi il tipo 79 è un sottotipo di number.
any implicito e annotazioni di tipo
A volte abbiamo bisogno di dichiarare una variabile prima che venga inizializzata, come orarioFine qui sotto:
// tra 500 e 1000 millisecondi
const const ATTESA_CASUALE: numberATTESA_CASUALE = var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.round(x: number): numberReturns a supplied numeric expression rounded to the nearest integer.round(var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): numberReturns a pseudorandom number between 0 and 1.random() * 500) + 500;
let let orarioInizio: DateorarioInizio = new var Date: DateConstructor
new () => Date (+4 overloads)
Date();
let let orarioFine: anyorarioFine;
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+2 overloads)Schedules execution of a one-time `callback` after `delay` milliseconds.
The `callback` will likely not be invoked in precisely `delay` milliseconds.
Node.js makes no guarantees about the exact timing of when callbacks will fire,
nor of their ordering. The callback will be called as close as possible to the
time specified.
When `delay` is larger than `2147483647` or less than `1` or `NaN`, the `delay`
will be set to `1`. Non-integer delays are truncated to an integer.
If `callback` is not a function, a `TypeError` will be thrown.
This method has a custom variant for promises that is available using
`timersPromises.setTimeout()`.setTimeout(() => {
let orarioFine: anyorarioFine = 0;
let orarioFine: anyorarioFine = new var Date: DateConstructor
new () => Date (+4 overloads)
Date();
}, const ATTESA_CASUALE: numberATTESA_CASUALE);orarioFine "nasce" senza un tipo esplicito, quindi finisce per essere un any implicito — passaci sopra con il mouse per vedertelo confermare dal compilatore.
Pensa ad any come "il normale modo di funzionare delle variabili in JS": potresti assegnare a orarioFine prima un number, poi una funzione, poi una stringa, senza che nessuno protesti.
TypeScript non ha abbastanza informazioni, nel punto in cui la variabile viene dichiarata, per inferire cosa dovrebbe essere orarioFine, quindi le assegna il tipo più permissivo possibile: any. Tornando al nostro paragone con gli insiemi, any rappresenta l'insieme { tutti i valori possibili } — è come uno scatolone senza etichetta, dentro ci va qualsiasi cosa.
Se vogliamo più sicurezza qui, possiamo aggiungere un'annotazione di tipo esplicita:
- let orarioFine
+ let orarioFine: Dateconst const ATTESA_CASUALE: numberATTESA_CASUALE = var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.round(x: number): numberReturns a supplied numeric expression rounded to the nearest integer.round(var Math: MathAn intrinsic object that provides basic mathematics functionality and constants.Math.Math.random(): numberReturns a pseudorandom number between 0 and 1.random() * 500) + 500;
let let orarioInizio: DateorarioInizio = new var Date: DateConstructor
new () => Date (+4 overloads)
Date();
let let orarioFine: DateorarioFine: Date;
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+2 overloads)Schedules execution of a one-time `callback` after `delay` milliseconds.
The `callback` will likely not be invoked in precisely `delay` milliseconds.
Node.js makes no guarantees about the exact timing of when callbacks will fire,
nor of their ordering. The callback will be called as close as possible to the
time specified.
When `delay` is larger than `2147483647` or less than `1` or `NaN`, the `delay`
will be set to `1`. Non-integer delays are truncated to an integer.
If `callback` is not a function, a `TypeError` will be thrown.
This method has a custom variant for promises that is available using
`timersPromises.setTimeout()`.setTimeout(() => {
orarioFine = 0; let orarioFine: DateorarioFine = new var Date: DateConstructor
new () => Date (+4 overloads)
Date();
}, const ATTESA_CASUALE: numberATTESA_CASUALE);Ora TypeScript ci avvisa correttamente quando proviamo a "far ballare" orarioFine tra il numero 0 e un Date.
Casting di tipo
Ci sono occasioni, specialmente mentre esplori TypeScript, in cui vuoi forzare il compilatore a considerare un valore come se fosse di un tipo particolare. Questo si chiama type casting.
let let fondazionePompeiTech: DatefondazionePompeiTech = new var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date("Jan 1, 2020");
let let data1: Datedata1 = let fondazionePompeiTech: DatefondazionePompeiTech;
let let data2: anydata2 = let fondazionePompeiTech: DatefondazionePompeiTech as any; // forziamo il tipo a "any"
Questa è una cosa che dovresti fare con molta cautela. A volte è sicuro fare il cast verso un tipo più generale, ma è potenzialmente pericoloso farlo verso un tipo più specifico o non correlato.
Ecco un esempio di cast sicuro (anche se piuttosto inutile):
const const umidita: numberumidita = 79 as number; // 79 è un number? Sì, quindi questo è sicuro!
ed ecco un esempio di cast non sicuro. Questo tipo di pattern fa sostanzialmente "mentire" TypeScript:
let let data3: Datedata3 = "oops" as any as Date;
let data3: Datedata3; // TypeScript ora pensa che sia un Date, ma in realtà è una stringa
let data3: Datedata3.Date.toISOString(): stringReturns a date as a string value in ISO format.toISOString(); // cosa pensiamo che succederà quando eseguiamo questo? 💥Nota che nell'esempio sopra dobbiamo prima fare il cast verso l'alto ad any, e solo dopo verso il basso a Date. TypeScript non ci permette nemmeno di fare il cast direttamente da string a Date, perché è pericoloso:
let let data4: Datedata4 = "oops" as Date;Argomenti e valori di ritorno delle funzioni
La sintassi : Date che abbiamo appena visto per le annotazioni di tipo sulle variabili si può usare anche per descrivere argomenti e valori di ritorno delle funzioni. In questo esempio non è chiaro, nemmeno guardando l'implementazione della funzione, se somma debba accettare numeri o stringhe:
function function somma(a: any, b: any): anysomma(a: anya, b: anyb) {
return a: anya + b: anyb; // stringhe? numeri? un mix?
}Ecco cosa mostrerebbe il tooltip del tuo editor se usassi questa funzione:
function function somma(a: any, b: any): anysomma(a: anya, b: anyb) {
return a: anya + b: anyb;
}
const const risultato: anyrisultato = function somma(a: any, b: any): anysomma(3, "4");
Senza annotazioni di tipo, "tutto è permesso" per gli argomenti passati a somma. Perché è un problema? Immagina di passare quel risultato al costruttore di una Promise, che si aspetta una funzione a due argomenti:
function function somma(a: any, b: any): anysomma(a: anya, b: anyb) {
return a: anya + b: anyb;
}
const const risultato: anyrisultato = function somma(a: any, b: any): anysomma(3, "4");
const const p: Promise<unknown>p = new var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>
Creates a new Promise.Promise(const risultato: anyrisultato);
Se hai mai creato una Promise usando il suo costruttore, saprai che ci si aspetta una funzione a due argomenti, non una stringa. Questo è esattamente il tipo di errore che vorremmo che TypeScript catturasse per noi.
Aggiungiamo qualche annotazione di tipo agli argomenti della nostra funzione:
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number) {
return a: numbera + b: numberb;
}
const const risultato: numberrisultato = function somma(a: number, b: number): numbersomma(3, "4");Ottimo, ora possiamo imporre che vengano passati alla funzione solo valori di tipo number, e TypeScript può determinare automaticamente il tipo di ritorno:
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number) {
return a: numbera + b: numberb;
}
const const risultato: numberrisultato = function somma(a: number, b: number): numbersomma(3, 4);
Se vogliamo dichiarare esplicitamente anche il tipo di ritorno, possiamo farlo con la stessa sintassi, in un altro punto della firma:
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number): number {}Questo è un ottimo modo per chi scrive il codice di dichiarare le proprie intenzioni fin da subito. TypeScript si assicura che manteniamo questa promessa, e gli errori vengono segnalati nel punto in cui la funzione è dichiarata, invece che nel punto in cui viene usato il valore che la funzione restituisce. Una volta implementato il corpo della funzione, l'errore sparisce:
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number): number {
return a: numbera + b: numberb;
}