Fastify & Typescript: with the new Node.js flag --experimental-strip-types

Read the article in italian language

Since Node.js version v22.6.0, a new experimental flag has been introduced:

--experimental-strip-types

This flag allows us to run a .ts file directly with Node.js without needing external libraries or compiling to JavaScript first, using the "type stripping" technique.

To use this new flag you need to use a Node.js version equal to or greater than v22.6.0

In this article I will be using Node.js 22.8.0

--experimental-strip-types in Node.js

Let's create a file, for example node-typescript.ts

interface Person {
    name: string
    surname: string
}

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

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

Let's try to run it with node:

node node-typescript.ts

And 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 what a nice error, and rightly so because "interface" does not exist in Javascript but is a Typescript feature.

This is where the new experimental flag comes into play, allowing us to run Typescript files directly with Node.js.

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

And now as output we will see:

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 it works!

We also see a warning that we are using an experimental flag.

--experimental-strip-types in Fastify

Let’s create the Fastify project

We use fastify-cli to create the Fastify project.

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

Let's go into the "fastify-type-stripping" folder we just created and install the dependencies

npm install

Now let's launch the newly created server

npm start

If all goes well we should see output like this:

{"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"}

Let’s use --experimental-strip-types

fastify-cli generated this 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"
  }
}

Let's analyze the package.json start script:

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

The "start" script consists of two commands:

  • npm run build:ts

  • fastify start -l info dist/app.js

So we see that it first runs the "build:ts" script:

"build:ts": "tsc"

"build:ts" runs the command "tsc", which converts the TypeScript code into JavaScript according to the settings found in the tsconfig.json file, which in our case is:

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

As we can see, there's the property "outDir": "dist", which tells the compiler to put the compilation output, that is, the generated JavaScript files, in the "dist" folder.

Then the command fastify start -l info dist/app.js is executed, which starts our server from the dist/app.js file autogenerated by the TypeScript compiler.

So we see that every time we start the server, there's always this intermediate step of converting TypeScript files to JavaScript. Then the server is actually started from the JavaScript files.

Let's launch the server without Typescript compilation

Let's try using the new Node.js feature --experimental-strip-types to directly launch the server with the TypeScript source code without first converting the files to JavaScript.

We'll proceed with trial and error and fix any issues we encounter.

Attempt 1: Initial State

Let's start by just removing the compilation and launching the server directly with the TypeScript file and enjoy the explosion:

"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!!!

We have an error telling us the .ts file is not supported, but we saw that we can add the new Node.js flag --experimental-strip-types for this.

But where?

Attempt 2: Passing the --experimental-strip-types flag

The --experimental-strip-types flag needs to be passed to the node executable, but we are launching the server with:
fastify start -l info src/app.ts. In this case, we can use the environment variable: NODE_OPTIONS which allows us to pass Node.js options like this:

"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, the error has changed!

Now we see that the flag --experimental-strip-types was correctly recognized because we have this 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)

But there is an error with the 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.

There is something wrong with an import in our src/app.ts file:

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

This is due to the behavior of the type stripping flag --experimental-strip-types. If we import a TypeScript type without explicitly using the keyword “type,” Node.js will treat it as a regular value, causing a runtime error, as explained in more detail here: https://nodejs.org/docs/latest-v22.x/api/typescript.html#type-stripping.

Attempt 3: Using “import type” for TypeScript types

To ensure we correctly import TypeScript types with “import type” so that type stripping can properly remove them, we add a TypeScript compiler setting: verbatimModuleSyntax to our file:

tsconfig.json

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

Now we will see the import errors directly from our editor:

So let's go fix all the errors:

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'

Now that we have solved all the import problems let's try to launch the server and cross our fingers.

 *  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 another error!

The error comes from the @fastify/autoload module and it tells us:
To fix this error, compile TypeScript to JavaScript or use 'ts-node' to run your app.

It is a check done by @fastify/autoload. To disable it, we need to start the server with the environment variable: FASTIFY_AUTOLOAD_TYPESCRIPT

Attempt 4: FASTIFY_AUTOLOAD_TYPESCRIPT

Let's also add the environment variable: FASTIFY_AUTOLOAD_TYPESCRIPT to the "start" script.

"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, it finally works!

Now we can run our server directly from the TypeScript source code without needing to compile it anymore.

We can do the same for the "test" script in package.json and remove the "ts-node" library from "devDependencies" because we can do it natively in Node.js thanks to the new --experimental-strip-types flag.

1
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