Il tuo primo programma TypeScript
Il tuo primo programma TypeScript
Immagina di dover far tradurre un contratto legale a un traduttore professionista. Prima di consegnarglielo, quel traduttore ti farà sempre una domanda: "per chi è questo testo?". Se il destinatario è un tribunale internazionale, servirà un linguaggio formale e tecnico. Se invece è un cliente straniero che deve solo capire i punti essenziali, andrà bene una versione semplificata. Il contenuto informativo del testo originale resta lo stesso, ma la "versione finale" cambia completamente in base a chi la deve leggere.
Il compilatore di TypeScript funziona in modo molto simile: prende il tuo codice, che contiene sia la logica che le informazioni sui tipi, e lo "traduce" in JavaScript puro, adattando l'output al "pubblico" a cui è destinato — che può essere un vecchio browser, l'ultima versione di Node.js, o qualsiasi altro ambiente di esecuzione tu abbia scelto. In questo capitolo vediamo come funziona in pratica questo processo di traduzione.
Anatomia di un progetto TypeScript
Il più piccolo progetto TypeScript sensato è composto da tre file:
package.json # Il manifesto del pacchetto npm
tsconfig.json # La configurazione del compilatore TypeScript
src/index.ts # "il programma" vero e proprioCreiamolo da zero. Apri il terminale, crea una cartella e inizializzala come progetto npm:
mkdir saluto-typescript
cd saluto-typescript
npm init -y
npm install --save-dev typescriptpackage.json — il nostro manifesto minimo:
{
"name": "saluto-typescript",
"license": "UNLICENSED",
"devDependencies": {
"typescript": "^5.6.0"
},
"scripts": {
"dev": "tsc --watch --preserveWatchOutput"
}
}Da notare:
- Abbiamo una sola dipendenza:
typescript. - Abbiamo uno script
dev, che esegue il compilatore in modalità "watch" (tiene d'occhio i file sorgenti e ricompila automaticamente a ogni modifica).
Ecco praticamente il file di configurazione più semplice possibile per il compilatore:
tsconfig.json:
{
"compilerOptions": {
"outDir": "dist", // dove mettere i file compilati
"target": "ES2015", // il "livello" del linguaggio JS di destinazione
"moduleResolution": "Node" // come trovare i moduli CommonJS in node_modules
},
"include": ["src"] // quali file compilare
}Tutte queste opzioni potrebbero essere specificate da riga di comando (per esempio tsc --outDir dist), ma man mano che un progetto cresce in complessità, avere questo file di configurazione centralizzato è un enorme vantaggio.
Infine, scriviamo un programma TypeScript relativamente semplice, ma con qualche caratteristica interessante che renderà più evidenti gli effetti delle modifiche alla proprietà target nel nostro tsconfig.json:
- L'uso del costruttore nativo
Promise(introdotto in ES2015). - L'uso di
asynceawait(introdotti in ES2017). - L'uso di un campo
staticprivato di classe (introdotto in ES2022).
src/index.ts:
/**
* Crea una promise che si risolve dopo un certo tempo
* @param n numero di millisecondi prima che la promise si risolva
*/
function timeout(n: number) {
return new Promise((res) => setTimeout(res, n));
}
/**
* Somma due numeri
* @param a primo numero
* @param b secondo numero
*/
export async function addNumbers(a: number, b: number) {
await timeout(500);
return a + b;
}
class Foo {
static #bar = 3;
static getValue() {
return Foo.#bar;
}
}
//== Esegui il programma ==//
(async () => {
console.log(await addNumbers(Foo.getValue(), 4));
})();Se apri questo file in Visual Studio Code e passi il mouse sopra un simbolo — per esempio il nome della funzione addNumbers — vedrai un tooltip con la sua firma completa. Questo è uno degli strumenti più importanti per imparare come TypeScript "capisce" il tuo codice: usalo spesso, anche solo per curiosità, mentre segui questi capitoli.
Eseguire il compilatore
Dalla cartella del progetto, puoi lanciare:
npx tscper compilare il progetto una volta sola. In alternativa, sempre dalla stessa cartella, puoi avviare il task che ricompila automaticamente ogni volta che modifichi un file importante:
npm run devDovresti vedere qualcosa del genere nel terminale:
12:01:57 - Starting compilation in watch mode...Nota che, all'interno della cartella saluto-typescript:
- È comparsa una cartella
./dist. - Al suo interno c'è un file
index.js.
Apri quel file: sarà un po' disordinato. Ecco un estratto di cosa contiene, compilato con target: "ES2015":
var __awaiter =
(this && this.__awaiter) ||
function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P
? value
: new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done
? resolve(result.value)
: adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
// ...segue codice equivalente ma "srotolato" a mano, incluso un helper
// per simulare i campi privati delle classi (#bar), non supportati
// nativamente da ES2015.Se conosci il vecchio standard ES5, noterai alcune cose che ci dicono che questo output punta a ES2015. Per esempio, l'uso della keyword yield, una funzione generatrice function*, le class e il costruttore Promise.
Cambiare il target del linguaggio
Se andiamo in tsconfig.json e modifichiamo la proprietà compilerOptions.target:
{
"compilerOptions": {
"outDir": "dist",
- "target": "ES2015"
+ "target": "ES2017"
},
"include": ["src"]
}e ricompiliamo, il file dist/index.js sarà molto più pulito. Cosa è cambiato?
- Iniziamo a vedere
asynceawaitdirettamente nell'output. - L'helper
__awaiternon c'è più.
Il codice comincia a somigliare molto di più all'originale: come se le informazioni sui tipi fossero semplicemente state rimosse dal file .ts di partenza. Rimane ancora qualche "trucco" del compilatore per simulare il campo privato di classe, che non è supportato nativamente in JavaScript fino a ES2022.
Proviamo, infine, con ES2022:
{
"compilerOptions": {
"outDir": "dist",
- "target": "ES2017"
+ "target": "ES2022"
},
"include": ["src"]
}Ricompilando, vedrai che anche il campo privato #bar viene emesso nativamente, senza alcun trucco aggiuntivo: a questo target, Node.js e i browser moderni supportano questa sintassi in modo nativo.
I file di dichiarazione
Se abiliti l'opzione declaration: true nel tsconfig.json, noterai che viene generato anche un file .d.ts come parte del processo di compilazione. Questo si chiama file di dichiarazione, e contiene esclusivamente le informazioni sui tipi, senza alcuna implementazione:
/**
* Somma due numeri
* @param a primo numero
* @param b secondo numero
*/
export declare function addNumbers(
a: number,
b: number,
): Promise<number>;Un buon modo per pensare ai diversi tipi di file:
- I file
.tscontengono sia le informazioni sui tipi che il codice che viene effettivamente eseguito. - I file
.jscontengono solo il codice che viene eseguito. - I file
.d.tscontengono solo le informazioni sui tipi.
Esistono altre estensioni che hanno un equivalente TypeScript:
| Scopo del file | Estensione JS | Estensione TS |
|---|---|---|
| React | .jsx | .tsx |
| Moduli ES nativi | .mjs | .mts |
Tipi di moduli
CommonJS
Hai notato che la keyword export era ancora presente nell'output compilato del nostro programma? Di default stiamo generando moduli ES2015 a partire dal sorgente TypeScript.
Se provassimo a eseguire questo file con node così com'è:
node dist/index.jsotterremmo un errore:
export async function addNumbers(a, b) {
^^^^^^
SyntaxError: Unexpected token 'export'Sembra che, almeno con le versioni più recenti di Node.js e con il modo in cui il nostro progetto è attualmente configurato, non possiamo eseguire direttamente questo programma così com'è.
Per convenzione Node.js si aspetta moduli CommonJS, quindi dobbiamo dire a TypeScript di generare questo tipo di output. Aggiungiamo una nuova proprietà al file tsconfig.json:
"compilerOptions": {
"outDir": "dist",
+ "module": "CommonJS",Ricompila e riguarda il file dist/index.js: noterai che il modo in cui la funzione addNumbers viene esportata è cambiato:
exports.addNumbers = addNumbers;Questo è un segnale che stiamo generando moduli CommonJS! Possiamo ora provare a eseguire il programma con node:
node dist/index.jsSe tutto funziona correttamente, dovresti vedere il programma fermarsi per un breve istante e poi stampare 7 in console, prima di terminare con successo.
Moduli ES
Node.js oggi supporta anche l'esecuzione diretta di moduli nativi (file .mjs)! Possiamo configurare TypeScript per generare questo tipo di output. Modifica così il tuo tsconfig.json:
"target": "ES2022",
+ "module": "NodeNext",
- "module": "CommonJS",
- "moduleResolution": "Node"Ricompila il progetto lanciando npx tsc dalla cartella saluto-typescript, poi esegui:
node dist/index.jsDovresti vedere di nuovo 7 stampato in console!
Complimenti, hai appena compilato ed eseguito il tuo primo programma TypeScript, osservando come cambia l'output a seconda del target del linguaggio e del sistema di moduli scelto.
