Pompei Tech
Guide · typescript

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:

ts
let 
let temperatura: number
temperatura
= 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:

ts
let let temperatura: numbertemperatura = 6;
temperatura = "calda";
Type 'string' is not assignable to type 'number'.

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:

ts
const 
const umidita: 79
umidita
= 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 const non 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:

text
{ 1, 2, 3 } // "1 oppure 2 oppure 3"

Torniamo ai nostri esempi di prima:

ts
let 
let temperatura: number
temperatura
= 19;
const
const umidita: 79
umidita
= 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:

ts
let let temperatura: numbertemperatura = 19;
let temperatura: numbertemperatura = 23; // nessun problema

Il 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:

ts
let 
let temperatura2: number
temperatura2
= 19;
let
let umidita2: 79
umidita2
= 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:

ts
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
Type 'number' is not assignable to type '79'.
let umidita3: 79umidita3 = 79; // OK umidita3 = 78; // non compatibile: 78 non rientra in 79
Type '78' is not assignable to type '79'.

Ognuna 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:

ts
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;
Type 'number' is not assignable to type '79'.
// Ogni membro di { 79 } è anche in { 79 }? let umid2: 79umid2 = 79; // Ogni membro di { 78 } è anche in { 79 }? umid2 = 78;
Type '78' is not assignable to type '79'.

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:

ts
// tra 500 e 1000 millisecondi
const const ATTESA_CASUALE: numberATTESA_CASUALE = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.round(x: number): number
Returns a supplied numeric expression rounded to the nearest integer.
@paramx The value to be rounded to the nearest integer.
round
(var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.random(): number
Returns 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: any
orarioFine
;
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()`.
@sincev0.0.1@paramcallback The function to call when the timer elapses.@paramdelay The number of milliseconds to wait before calling the `callback`. **Default:** `1`.@paramargs Optional arguments to pass when the `callback` is called.@returnsfor use with `clearTimeout()`
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:

diff
- let orarioFine
+ let orarioFine: Date
ts
const const ATTESA_CASUALE: numberATTESA_CASUALE = var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.round(x: number): number
Returns a supplied numeric expression rounded to the nearest integer.
@paramx The value to be rounded to the nearest integer.
round
(var Math: Math
An intrinsic object that provides basic mathematics functionality and constants.
Math
.Math.random(): number
Returns 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()`.
@sincev0.0.1@paramcallback The function to call when the timer elapses.@paramdelay The number of milliseconds to wait before calling the `callback`. **Default:** `1`.@paramargs Optional arguments to pass when the `callback` is called.@returnsfor use with `clearTimeout()`
setTimeout
(() => {
orarioFine = 0;
Type 'number' is not assignable to type 'Date'.
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.

ts
let let fondazionePompeiTech: DatefondazionePompeiTech = new 
var Date: DateConstructor
new (value: number | string | Date) => Date (+4 overloads)
Date
("Jan 1, 2020");
let
let data1: Date
data1
= let fondazionePompeiTech: DatefondazionePompeiTech;
let
let data2: any
data2
= 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):

ts
const 
const umidita: number
umidita
= 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:

ts
let let data3: Datedata3 = "oops" as any as Date;
let data3: Date
data3
; // TypeScript ora pensa che sia un Date, ma in realtà è una stringa
let data3: Datedata3.Date.toISOString(): string
Returns 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:

ts
let let data4: Datedata4 = "oops" as Date;
Conversion of type 'string' to type 'Date' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

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:

ts
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:

ts
function function somma(a: any, b: any): anysomma(a: anya, b: anyb) {
  return a: anya + b: anyb;
}
const 
const risultato: any
risultato
= 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:

ts
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.
@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
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:

ts
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");
Argument of type 'string' is not assignable to parameter of type 'number'.

Ottimo, ora possiamo imporre che vengano passati alla funzione solo valori di tipo number, e TypeScript può determinare automaticamente il tipo di ritorno:

ts
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number) {
  return a: numbera + b: numberb;
}
const 
const risultato: number
risultato
= 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:

ts
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number): number {}
A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.

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:

ts
function function somma(a: number, b: number): numbersomma(a: numbera: number, b: numberb: number): number {
  return a: numbera + b: numberb;
}