Configurare TypeScript con Node.js

LT
Luca Terribili
Autore

Nel precedente tutorial ho preparato l'ambiente di sviluppo per il mio progetto backend, installando Node.js, npm, Express e gli strumenti necessari per lavorare con TypeScript. Adesso è arrivato il momento di configurare il compilatore e capire come utilizzare TypeScript all'interno di un progetto Node.js.

Ho scelto TypeScript perché, quando un backend comincia a crescere, scrivere codice JavaScript senza controlli sui tipi può diventare complicato. All'inizio sembra tutto semplice: creo una variabile, richiamo una funzione e restituisco una risposta HTTP. Poi aggiungo altri moduli, collego il database, gestisco gli errori e mi ritrovo a inseguire problemi che avrei potuto individuare prima ancora di eseguire il programma.

TypeScript aggiunge un sistema di tipi statici a JavaScript. In pratica, posso dichiarare che una variabile deve contenere un numero, che una funzione deve restituire una determinata struttura di dati oppure che un parametro è facoltativo. Il compilatore controlla queste informazioni e segnala numerosi errori prima che il codice venga eseguito.

È importante chiarire subito una cosa: Node.js non esegue direttamente i file TypeScript come se fossero normali file JavaScript. Per lavorare comodamente durante lo sviluppo posso utilizzare strumenti come tsx, mentre per controllare il codice e generare i file JavaScript utilizzo il compilatore TypeScript, chiamato tsc.

In questo tutorial configurerò TypeScript, analizzerò le opzioni principali di tsconfig.json e realizzerò un piccolo programma di prova. L'obiettivo non è ancora costruire un'API REST, ma predisporre una base solida sulla quale svilupperò il backend nei prossimi articoli.

Verificare TypeScript e le dipendenze

Prima di modificare la configurazione, verifico che gli strumenti installati nei tutorial precedenti siano effettivamente disponibili. Lavoro dalla directory del progetto, che nel mio caso si trova in ~/dev/node-study.

cd ~/dev/node-study

Per controllare la versione di TypeScript eseguo:

npx tsc --version

Il comando mostra la versione del compilatore installato nel progetto. Nel mio ambiente il risultato è Version 7.0.2. Utilizzo npx perché permette di eseguire il compilatore installato localmente, senza dover installare TypeScript globalmente sul sistema.

Verifico anche le dipendenze del progetto:

npm ls --depth=0

Questo comando elenca i pacchetti installati direttamente, mostrando le rispettive versioni. Nel mio progetto sono presenti TypeScript, Express, le definizioni di tipo di Node.js ed Express e tsx. Questi strumenti hanno responsabilità differenti: Express gestirà le richieste HTTP, TypeScript controllerà il codice e tsx consentirà di eseguire i file TypeScript durante lo sviluppo.

Non è necessario reinstallare i pacchetti se sono già presenti. Prima di aggiungere dipendenze, preferisco verificare quello che ho: è un'abitudine semplice che evita installazioni duplicate e configurazioni inutilmente complicate.

Configurare i moduli JavaScript

Prima di creare tsconfig.json, controllo un'impostazione importante del file package.json:

npm pkg get type

Nel mio progetto avevo inizialmente il valore commonjs. Per questo backend ho scelto invece gli ES Modules, il sistema moderno di moduli JavaScript basato sulle istruzioni import ed export.

Modifico quindi il valore con:

npm pkg set type=module

Il comando aggiorna il campo type di package.json, impostandolo a module. Da questo momento, per impostazione predefinita, Node.js interpreta i file .js del pacchetto come moduli ES.

Questa scelta non ha niente a che vedere con Next.js. La somiglianza tra il nome NodeNext, che utilizzerò nella configurazione TypeScript, e quello del framework Next.js è puramente lessicale.

Creare il file tsconfig.json

TypeScript utilizza il file tsconfig.json per stabilire come analizzare e compilare il progetto. In questo file definisco le opzioni del compilatore, la directory dei sorgenti e quella in cui verranno generati i file JavaScript.

Creo innanzitutto la directory che conterrà il codice sorgente:

mkdir -p src

L'opzione -p permette di creare la directory senza segnalare un errore se esiste già. Ho scelto src perché è una convenzione comune per organizzare il codice sorgente di un'applicazione.

A questo punto creo il file di configurazione:

cat > tsconfig.json <<'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "rootDir": "src",
    "outDir": "dist",
    "skipLibCheck": true,
    "noEmitOnError": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}
EOF

Il comando scrive il contenuto indicato nel file tsconfig.json. La sequenza EOF delimita il testo da inserire e impedisce alla shell di interpretare il contenuto come una serie di comandi. È un modo pratico per creare un file di configurazione direttamente dal terminale.

Vediamo ora perché ho scelto queste opzioni.

Capire le opzioni del compilatore

L'opzione target stabilisce la versione di JavaScript verso cui TypeScript deve compilare il codice. Ho scelto ES2022, una versione moderna del linguaggio, adatta al nostro ambiente Node.js. Non significa che TypeScript smetterà di controllare i tipi: stabilisce soprattutto il livello di JavaScript da generare e quali trasformazioni sintattiche siano necessarie.

Le opzioni module e moduleResolution sono entrambe impostate a NodeNext. La prima permette a TypeScript di seguire le regole dei moduli di Node.js quando interpreta e genera i moduli; la seconda stabilisce come risolvere gli import. In questo modo la configurazione tiene conto del comportamento di Node.js e del campo type presente in package.json.

NodeNext non installa Next.js e non richiede di utilizzare quel framework. È semplicemente il nome della modalità di TypeScript che segue le regole moderne dei moduli Node.js.

L'opzione strict attiva i controlli più rigorosi sui tipi. Per esempio, aiuta a individuare situazioni in cui una variabile potrebbe contenere null o undefined quando il programma si aspetta un valore valido. Preferisco abilitare questi controlli fin dall'inizio, invece di ritrovarmi a correggere decine di problemi quando il backend sarà già cresciuto.

Con esModuleInterop miglioro la compatibilità con alcune modalità di importazione dei moduli CommonJS. È utile quando si lavora con librerie JavaScript che non utilizzano tutte lo stesso sistema di moduli.

Le opzioni rootDir e outDir stabiliscono rispettivamente la directory dei sorgenti e quella dei file generati. Nel nostro caso, il codice TypeScript si troverà in src, mentre il JavaScript compilato finirà in dist. Separare i due ambienti rende il progetto più ordinato e impedisce di confondere il codice che modifico con quello prodotto dal compilatore.

Infine, skipLibCheck evita di controllare integralmente i file di dichiarazione dei tipi delle dipendenze. noEmitOnError impedisce invece al compilatore di generare i file di output quando la compilazione presenta errori. Entrambe sono impostazioni utili, ma non sostituiscono i controlli sul codice applicativo.

Le proprietà include ed exclude indicano quali file TypeScript includere nel progetto e quali directory escludere. In questo caso considero i file .ts contenuti in src, escludendo esplicitamente node_modules e dist.

Verificare la configurazione

Dopo aver creato il file, controllo che il progetto utilizzi effettivamente i moduli ES:

npm pkg get type

Il risultato atteso è:

"module"

Verifico quindi la configurazione TypeScript effettiva:

npx tsc --showConfig

L'opzione --showConfig mostra la configurazione risultante dopo che TypeScript ha letto il file tsconfig.json e applicato le proprie regole. Posso così controllare che target, module, moduleResolution e le altre opzioni siano state recepite correttamente.

Non eseguo ancora una compilazione completa se la directory src non contiene file TypeScript: prima creo un programma di prova. In questo modo verifico non soltanto che la configurazione sia sintatticamente valida, ma anche che funzioni nel contesto reale del progetto.

Creare il primo file TypeScript

Creo il file src/index.ts, che utilizzerò come punto di partenza per il nostro progetto:

cat > src/index.ts <<'EOF'
const message: string = 'TypeScript funziona correttamente';

console.log(message);
EOF

Il programma contiene soltanto una variabile e una chiamata a console.log, ma introduce già un concetto importante. La dichiarazione const message: string indica che message deve contenere una stringa. Se provassi ad assegnarle un numero, TypeScript potrebbe segnalare un errore di tipo.

La parte : string non viene mantenuta nel JavaScript generato, perché è un'informazione utilizzata durante l'analisi statica del codice. Il risultato finale sarà un normale programma JavaScript che Node.js può eseguire.

Per verificare il comportamento durante lo sviluppo utilizzo tsx:

npx tsx src/index.ts

Se tutto è configurato correttamente, il terminale mostra:

TypeScript funziona correttamente

tsx consente di eseguire il file TypeScript senza dover compilare manualmente prima ogni modifica. È particolarmente comodo durante lo sviluppo, quando voglio provare rapidamente una modifica e vedere il risultato.

C'è però una distinzione fondamentale: eseguire un file con tsx non equivale a effettuare un controllo completo dei tipi con tsc. tsx trasforma ed esegue il codice, ma non sostituisce il controllo statico del compilatore TypeScript.

Controllare i tipi con tsc

Per verificare che il codice rispetti le regole definite in tsconfig.json, eseguo:

npx tsc --noEmit

L'opzione --noEmit dice al compilatore di controllare il progetto senza generare file JavaScript. Se non vengono segnalati errori, il controllo è terminato correttamente.

Per capire perché questo passaggio è importante, immagino di modificare temporaneamente la variabile del nostro esempio in questo modo:

const age: number = '49';

La variabile è dichiarata come numero, ma il valore assegnato è una stringa. Il compilatore rileva l'incompatibilità e segnala un errore. La differenza può sembrare banale, ma in un backend dati di questo tipo arrivano spesso da richieste HTTP, database e servizi esterni. Individuare le incompatibilità prima dell'esecuzione può evitare diversi problemi.

Una precisazione: TypeScript non valida automaticamente i dati ricevuti a runtime. I tipi statici aiutano a controllare il codice durante lo sviluppo, ma per verificare dati esterni dovremo introdurre anche una validazione esplicita. Ci occuperemo di questo quando costruiremo le API.

Dopo aver effettuato la prova, ripristino il codice corretto e rieseguo npx tsc --noEmit. In questo modo il progetto rimane in uno stato funzionante.

Compilare il progetto ed eseguire JavaScript

Finora ho eseguito il file con tsx e controllato i tipi con tsc --noEmit. Adesso voglio verificare anche il processo di compilazione vero e proprio.

Eseguo:

npx tsc

TypeScript legge la configurazione, controlla il codice e genera i file JavaScript nella directory dist, rispettando le impostazioni definite in tsconfig.json.

Posso verificare il risultato con:

find dist -maxdepth 2 -type f

Nel nostro caso dovrebbe comparire dist/index.js. Il file generato contiene il codice JavaScript che Node.js può eseguire direttamente.

Per provarlo, utilizzo:

node dist/index.js

Il risultato atteso è ancora:

TypeScript funziona correttamente

Questo test dimostra che il percorso completo funziona: scrivo il codice TypeScript, controllo i tipi, compilo il progetto e infine eseguo il JavaScript prodotto.

Durante lo sviluppo continuerò a utilizzare tsx per la comodità di esecuzione. La compilazione con tsc, invece, sarà importante quando vorrò verificare il progetto e preparare i file da eseguire nell'ambiente di destinazione.

Conclusione

A questo punto ho configurato TypeScript nel progetto Node.js, scelto il sistema ES Modules e separato il codice sorgente dai file generati. Ho anche verificato la differenza tra eseguire un file TypeScript con tsx, controllare i tipi con tsc --noEmit e compilare il progetto con tsc.

La struttura di partenza è semplice: src conterrà il codice che scriverò, dist ospiterà quello generato e tsconfig.json definirà le regole con cui TypeScript dovrà lavorare. È una base sufficientemente ordinata per aggiungere nuove funzionalità senza complicare inutilmente il progetto.

Nel prossimo tutorial passerò alla creazione del server Express. Trasformerò quindi questo piccolo programma di prova in un backend capace di avviarsi e ricevere richieste HTTP, preparando il terreno per le API REST e per l'integrazione con MongoDB.