Demo: running TypeScript directly in Node.js

[2025-02-20] dev, typescript
(Ad, please don’t block)

I have published the repository nodejs-type-stripping which demonstrates how to implement a package with a bin script that is written directly in TypeScript (no transpilation).

Features used in the repository  

File system layout of the repository  

nodejs-type-stripping/
  package.json
  tsconfig.json
  src/
    twice.ts
    util_test.ts
    util.ts
  • twice.ts implements the CLI command.

Running the CLI app  

npm install is only required if you need @types/node with types for Node.js APIs during editing.

cd nodejs-type-stripping/
node src/twice.ts

Alternative on Unix that hides ExperimentalWarning:

cd nodejs-type-stripping/
chmod u+x src/twice.ts
./src/twice.ts

package.json  

This repository only has a single dependency – the types for Node’s APIs:

"devDependencies": {
  "@types/node": "^22.13.4"
}

The bin script twice points directly to TypeScript source code:

"bin": {
  "twice": "./src/twice.ts"
}

We need the following property so that .ts files are interpreted as ESM modules (the rules are the same as for .js files):

"type": "module"

Package scripts:

"scripts": {
  "tsc": "tsc",
  "test": "node --test"
}
  • Due to "noEmit":true in tsconfig.json, running tsc type-checks all TypeScript files in the repository. This package script exists in case I want to install TypeScript locally inside this repository.
  • By default, node --test finds all *_test.ts files (among others) and runs them.

tsconfig.json  

I used the template from my blog post “A guide to tsconfig.json.

Important compilerOptions:

"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true, // TS 5.8+
"noEmit": true,
// Only needed if additionally compiling to JavaScript:
"rewriteRelativeImportExtensions": true,
  • In Node.js TypeScript code, we use the filename extension .ts in imports. Hence allowImportingTsExtensions.

  • erasableSyntaxOnly (available since TypeScript 5.8) ensure that we only use TypeScript features that can by stripped – e.g., we can’t use JSX or enums.

  • I only prevented the emission of files via noEmit, I did not remove related options such as outDir – in case I later want to additionally transpile. That is made possible by rewriteRelativeImportExtensions.

More information