Fastify & Typescript: con il nuovo flag --experimental-strip-types di Node.js

Read the article in english language

Dalla la versione di Node.js v22.6.0 è stato introdotto il nuovo flag sperimentale:

--experimental-strip-types

Questo flag ci permette di eseguire con Node.js un file .ts direttamente e senza bisogno di librerie esterne ne di compilare in Javascript prima di eseguire il file, usando la tecnica del "type stripping"

Per usare questo nuovo flag bisogna usare una versione di Node.js uguale o maggiore della v22.6.0

In questo articolo userò Node.js 22.8.0

--experimental-strip-types in Node.js

Creiamo un file, ad esempio node-typescript.ts

interface Person {
    name: string
    surname: string
}

const manuel: Person = {
    name: 'Manuel',
    surname: 'Salinardi'
} 

console.log(`Hello ${manuel.name} ${manuel.surname}`)

Proviamo ad eseguirlo con node:

node node-typescript.ts

E boom!

interface Person {
          ^^^^^^

SyntaxError: Unexpected identifier 'Person'
    at wrapSafe (node:internal/modules/cjs/loader:1469:18)
    at Module._compile (node:internal/modules/cjs/loader:1491:20)
    at Module._extensions..js (node:internal/modules/cjs/loader:1691:10)
    at Module.load (node:internal/modules/cjs/loader:1317:32)
    at Module._load (node:internal/modules/cjs/loader:1127:12)
    at TracingChannel.traceSync (node:diagnostics_channel:315:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:217:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:166:5)
    at node:internal/main/run_main_module:30:49

Wow che bell'errore, ed è giusto che sia così perché "interface" non esiste in Javascript ma è una feature di Typescript.

Ed è qui che entra in gioco il nuovo flag sperimentale che ci permette di eseguire file Typescript direttamente con Node.js

node --experimental-strip-types node-typescript.ts

Ed ora come output vedremo:

Hello Manuel Salinardi
(node:98118) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

Wow funziona!

Vediamo anche che c'è un warning che ci avvisa che stiamo usando un flag sperimentale.

--experimental-strip-types in Fastify

Creiamo il progetto Fastify

Usiamo fastify-cli per creare il progetto Fastify

npx fastify-cli generate fastify-type-stripping --lang=ts --esm

Entriamo nella cartella "fastify-type-stripping" appena creata e installiamo le dipendenze

npm install

Ora lanciamo il server appena creato

npm start

Se tutto è andato a buon fine dovremmo vedere un output come questo:

{"level":30,"time":1725951422786,"pid":8786,"hostname":"Manuels-MacBook-Pro.local","msg":"Server listening at http://127.0.0.1:3000"}
{"level":30,"time":1725951422787,"pid":8786,"hostname":"Manuels-MacBook-Pro.local","msg":"Server listening at http://[::1]:3000"}

Usiamo --experimental-strip-types

fastify-cli ha generato questo package.json

{
  "type": "module",
  "name": "fastify-type-stripping",
  "version": "1.0.0",
  "description": "This project was bootstrapped with Fastify-CLI.",
  "main": "app.ts",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "npm run build:ts && tsc -p test/tsconfig.json && FASTIFY_AUTOLOAD_TYPESCRIPT=1 node --test --experimental-test-coverage --loader ts-node/esm test/**/*.ts",
    "start": "npm run build:ts && fastify start -l info dist/app.js",
    "build:ts": "tsc",
    "watch:ts": "tsc -w",
    "dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
    "dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "fastify": "^5.0.0",
    "fastify-plugin": "^5.0.0",
    "@fastify/autoload": "^6.0.0",
    "@fastify/sensible": "^6.0.0",
    "fastify-cli": "^7.0.1"
  },
  "devDependencies": {
    "@types/node": "^22.1.0",
    "c8": "^10.1.2",
    "ts-node": "^10.4.0",
    "concurrently": "^9.0.0",
    "fastify-tsconfig": "^2.0.0",
    "typescript": "^5.2.2"
  }
}

Analizziamo lo script di start del package.json:

"start": "npm run build:ts && fastify start -l info dist/app.js"

Lo script “start“ è composto da due comandi:

  • npm run build:ts

  • fastify start -l info dist/app.js

Quindi vediamo che per prima cosa lancia lo script "build:ts":

"build:ts": "tsc"

"build:ts" lancia il comando “tsc”, Che converte il codice Typescript in JavaScript a seconda delle impostazioni che trova nel file tsconfig.json, che nel nostro caso è:

{
  "extends": "fastify-tsconfig",
  "compilerOptions": {
    "outDir": "dist",
    "sourceMap": true,
    "moduleResolution": "NodeNext",
    "module": "NodeNext",
    "target": "ES2022",
    "esModuleInterop": true
  },
  "include": ["src/**/*.ts"]
}

Come possiamo vedere c’è la proprietà "outDir": "dist". Che dice al compilatore di mettere l’output della compilazione, ovvero i file JavaScript generati nella cartella “dist“

Poi viene lanciato il comando: fastify start -l info dist/app.js, che va a lanciare il nostro server dal file dist/app.js autogenerato dal compilatore TypeScript.

Quindi vediamo che ogni volta che lanciamo il server c’è sempre questo passo intermedio che è la conversione dei file TypeScript in JavaScript e poi il server viene effettivamente lanciato dai file JavaScript.

Lanciamo il server senza la compilazione Typescript

Proviamo ad usare la nuova feature di Node.js --experimental-strip-types per lanciare direttamente il server con il codice sorgente TypeScriprt senza dover prima convertire i file in JavaScript.

Procediamo a tentativi e risolviamo tutti gli errori che incontriamo.

Tentativo 1: Stato iniziale

Iniziamo solo con provare a rimuovere la compilazione ed avviare il server direttamente con il file TypeScript e godiamoci l’esplosione:

"start": "fastify start -l info src/app.ts"
> fastify-type-stripping@1.0.0 start
> fastify start -l info src/app.ts

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/src/app.ts
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:217:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:243:36)
    at defaultLoad (node:internal/modules/esm/load:123:22)
    at async ModuleLoader.load (node:internal/modules/esm/loader:567:7)
    at async ModuleLoader.moduleProvider (node:internal/modules/esm/loader:442:45)
    at async ModuleJob._link (node:internal/modules/esm/module_job:106:19) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Boom!!!

Abbiamo un errore che ci dice il file .ts non è supportato, ma abbiamo visto che per questo possiamo aggiungere il nuovo flag di Node.js --experimental-strip-types.

Ma dove?

Tentativo 2: Passare il flag --experimental-strip-types

Il flag --experimental-strip-types va passato all’eseguibile node ma noi stiamo lanciando il server con:
fastify start -l info src/app.ts. In questo caso possiamo usare la variabile d’ambiente:
NODE_OPTIONS che ci permette di passare le opzioni di Node.js in questo modo:

"start": "NODE_OPTIONS='--experimental-strip-types' fastify start -l info src/app.ts"
 *  Executing task: source ~/.zshrc && npm run start 


> fastify-type-stripping@1.0.0 start
> NODE_OPTIONS='--experimental-strip-types' fastify start -l info src/app.ts

(node:57588) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
file:///Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/src/app.ts:2
import AutoLoad, {AutoloadPluginOptions} from '@fastify/autoload';
                  ^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Named export 'AutoloadPluginOptions' not found. The requested module '@fastify/autoload' is a CommonJS module, which m
ay not support all module.exports as named exports.                                                                                CommonJS modules can always be imported via the default export, for example using:

import pkg from '@fastify/autoload';
const {AutoloadPluginOptions} = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:171:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:254:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:482:26)
    at async requireServerPluginFromPath (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/fastify-c
li/util.js:83:22)                                                                                                                      at async runFastify (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/fastify-cli/start.js:115:1
2)                                                                                                                                 
 *  The terminal process "/bin/zsh '-l', '-c', 'source ~/.zshrc && npm run start'" terminated with exit code: 1.

Wow è cambiato l’errore!

Ora vediamo che il flag --experimental-strip-types é stato preso correttamente perchè abbiamo questo warning:

(node:57588) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

Ma c’è un errore con il plugin @fastify/autoload:

SyntaxError: Named export 'AutoloadPluginOptions' not found. The requested module '@fastify/autoload' is a CommonJS module, which m
ay not support all module.exports as named exports.

C’è qualcosa che non va con un import nel nostro file src/app.ts:

import AutoLoad, {AutoloadPluginOptions} from '@fastify/autoload';

Questo è dovuto dal comportamento del type stripping del flag --experimental-strip-types che se importiamo un typo TypeScript senza esplicitamente usare la keyword “type“ Node.js lo tratterà come un normale valore generando un errore a runtime, come spiegato più in dettaglio qui: https://nodejs.org/docs/latest-v22.x/api/typescript.html#type-stripping.

Tentativo 3: Usare “import type” per i tipi TypeScript

Per assicurarci di importare correttamente con “import type“ i tipi TypeScript in modo che il type stripping riesca correttamente ad eliminarli aggiungiamo una impostazione del compilatore TypeScript: verbatimModuleSyntax al nostro file:

tsconfig.json

{
  "extends": "fastify-tsconfig",
  "compilerOptions": {
    "outDir": "dist",
    "sourceMap": true,
    "moduleResolution": "NodeNext",
    "module": "NodeNext",
    "target": "ES2022",
    "esModuleInterop": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*.ts"]
}

Ora vedremo gli errori degli import direttamente dal nostro editor:

Quindi andiamo a sistemare tutti gli errori:

src/app.ts

import AutoLoad, { type AutoloadPluginOptions } from '@fastify/autoload';
import { type FastifyPluginAsync } from 'fastify';

src/routes/root.ts

import { type FastifyPluginAsync } from 'fastify'

src/routes/example/index.ts

import { type FastifyPluginAsync } from 'fastify'

src/plugins/sensible.ts

import sensible, { type FastifySensibleOptions } from '@fastify/sensible'

Ora che abbiamo risolto tutti i problemi di import proviamo a lanciare il server ed incrociamo le dita.

 *  Executing task: source ~/.zshrc && npm run start 


> fastify-type-stripping@1.0.0 start
> NODE_OPTIONS='--experimental-strip-types' fastify start -l info src/app.ts

(node:24423) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/lib/find-plugins.js:148
    throw new Error(`@fastify/autoload cannot import ${isHook ? 'hooks ' : ''}plugin at '${file}'. To fix this error compile TypeScript to JavaScrip
t or use 'ts-node' to run your app.`)                                                                                                                         ^

Error: @fastify/autoload cannot import plugin at '/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/src/plugins/sensible.ts'. To fi
x this error compile TypeScript to JavaScript or use 'ts-node' to run your app.                                                                         at handleTypeScriptSupport (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/lib/find-plugins.j
s:148:11)                                                                                                                                               at processFile (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/lib/find-plugins.js:124:3)
    at processDirContents (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/lib/find-plugins.js:101
:7)                                                                                                                                                     at buildTree (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/lib/find-plugins.js:41:9)
    at async findPlugins (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/lib/find-plugins.js:14:3
)                                                                                                                                                       at async autoload (/Users/manuelsalinardi/coding/blogs/fastify/fastify-type-stripping/node_modules/@fastify/autoload/index.js:20:22)

Node.js v22.8.0

 *  The terminal process "/bin/zsh '-l', '-c', 'source ~/.zshrc && npm run start'" terminated with exit code: 1.

Boom! Oh no un altro errore!

L’errore arriva dal modulo @fastify/autoload e ci dice:
To fix this error compile TypeScript to JavaScript or use 'ts-node' to run your app.

È un controllo che viene fatto da @fastify/autoload, per disabilitarlo bisogna lanciare il server con la variabile d’ambiente: FASTIFY_AUTOLOAD_TYPESCRIPT

Tentativo 4: FASTIFY_AUTOLOAD_TYPESCRIPT

Aggiungiamo anche la variabile d’ambiente: FASTIFY_AUTOLOAD_TYPESCRIPT allo script di “start

"start": "NODE_OPTIONS='--experimental-strip-types' FASTIFY_AUTOLOAD_TYPESCRIPT=1 fastify start -l info src/app.ts"
 *  Executing task: source ~/.zshrc && npm run start 


> fastify-type-stripping@1.0.0 start
> NODE_OPTIONS='--experimental-strip-types' FASTIFY_AUTOLOAD_TYPESCRIPT=1 fastify start -l info src/app.ts

(node:36950) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
{"level":30,"time":1727709779182,"pid":36950,"hostname":"Manuels-MacBook-Pro.local","msg":"Server listening at http://127.0.0.1:3000"}
{"level":30,"time":1727709779183,"pid":36950,"hostname":"Manuels-MacBook-Pro.local","msg":"Server listening at http://[::1]:3000"}

Wow finalmente funziona!

Ora siamo in grado di lanciare il nostro server direttamente dal codice sorgente TypeScript senza più il bisogno di compilazione.

Possiamo fare lo stesso anche per lo script di “test“ del package.json ed eliminare dalle “devDependencies“ la libreria “ts-node“ perchè lo stesso lo possiamo fare nativamente in Node.js grazie al nuovo flag --experimental-strip-types

0
Subscribe to my newsletter

Read articles from Manuel Salinardi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Manuel Salinardi
Manuel Salinardi