ts-node: ERR_UNKNOWN_FILE_EXTENSION on Node v20.0.0

Search Terms

Node, ERR_UNKNOWN_FILE_EXTENSION

Expected Behavior

Fix it

Actual Behavior

see Minimal reproduction

Steps to reproduce the problem

see Minimal reproduction

Minimal reproduction

$ cat example.ts
console.log('example')
$ cat tsconfig.json
{ "ts-node": { "esm": true } }
$ npx ts-node example.ts
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/septs/Projects/example/example.ts
    at new NodeError (node:internal/errors:399:5)
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:99:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:139:38)
    at defaultLoad (node:internal/modules/esm/load:83:20)
    at nextLoad (node:internal/modules/esm/hooks:781:28)
    at load (/Users/septs/.npm/_npx/1bf7c3c15bf47d04/node_modules/ts-node/dist/child/child-loader.js:19:122)
    at nextLoad (node:internal/modules/esm/hooks:781:28)
    at Hooks.load (node:internal/modules/esm/hooks:381:26)
    at handleMessage (node:internal/modules/esm/worker:153:24)
    at checkForMessages (node:internal/modules/esm/worker:102:28) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}

Specifications

  • ts-node version: 10.9.1

  • node version: 20.0.0

  • TypeScript version: 5.0

  • tsconfig.json:

    { "ts-node": { "esm": true } }
    
  • package.json:

    {}
    
  • Operating system and version: macOS 13.3.1

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 300
  • Comments: 115

Commits related to this issue

Most upvoted comments

I went forward and downgraded my node version to lts. However, doing node --loader ts-node/esm {file} did work for me.

Yes, node --no-warnings=ExperimentalWarning --loader ts-node/esm file.ts helps to bypass the issue until it’s fixed.

I have the same issue. I tried using --esm, { "esm": true } and --loader="ts-node/register", but got the ERR_UNKNOWN_FILE_EXTENSION no matter what I tried. Could not get ts-node to run on Node v20 with native ESM modules.

+1

The issue also appears when using the specially dedicated ESM runner

$ ts-node-esm file.ts

FYI Node 20 has made a breaking change:

Custom ESM loader hooks run on dedicated thread ESM hooks supplied via loaders (–experimental-loader=foo.mjs) now run in a dedicated thread, isolated from the main thread. This provides a separate scope for loaders and ensures no cross-contamination between loaders and application code.

https://nodejs.org/en/blog/release/v20.0.0

There are so many comments here that I thought it may be useful to have a summary:

There are many incompatiblities with Node.js v20.11.0 (current LTS release) and ts-node v10.9.2 (current release). It comes down to the following:

When using Node.js 18 and CommonJS, you can use the following start scripts:

  • ts-node src/main.ts
  • node --require ts-node/register src/main.ts

When using Node.js 18 and ESM, you can use:

  • ts-node-esm src/main.ts
  • node --loader ts-node/esm src/main.ts
  • node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts (if you don’t want to see experimental warnings)

When using Node.js 20 and ESM, you cannot use ts-node-esm anymore (it will show you ERR_UNKNOWN_FILE_EXTENSION). You have to use:

  • node --loader ts-node/esm src/main.ts

Unfortunately, this won’t give you stacktraces anymore. So if there is a compiler error, you will only get to see:

internalBinding(‘errors’).triggerUncaughtException

The TypeScript runner tsx won’t help in this situation as it doesn’t provide type checking capabilities.

You can work around this by using a combination of tsc and ts-node such as:

  • tsc --noEmit && node --loader ts-node/esm src/main.ts

Alternatively, you can use tsimp:

  • TSIMP_DIAG=error node --import=tsimp/import src/main.ts

I also made a tutorial showing each step:

i expirience the same issue on: Node v20, I tried: $ node --loader ts-node/esm ./path It worked for me but getting this warnning: ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time

This is horrible. It’s been such a long time now and this is still unfixed. @blakeembrey

👋 I deleted the *lock files with node_modules and reinstalled the dependencies. In my case, it helped me!

The best solution I found is to give up and switch to tsx - which, as an added bonus, is much faster too.

If you are using jest with ts-jest you also need to switch to babel before ts-node is completely scrubbed from your repo, and then this whole world of pain is but a memory and you can leave this thread and go on with your life 👍

When will you release a version that fixes this issue ? Last ts-node version is more than one year old 😦

This works for me out of the box using tsx npm i -D tsx npx tsx src/index.ts

Don’t know if this is an acceptable work around for most people here but we switched to tsx https://github.com/esbuild-kit/tsx for the time being as upgrading to Node v20 (the now current LTS) was important to us.

Thanks to the community for creating awareness for this issue. I was just wondering why my app on Heroku is suddenly crashing and then noticed that it is running on Node v20 as it is Heroku’s default since 31st October.

Switching from ts-node-esm to node --no-warnings --loader ts-node/esm solved the issue for me. 👍

yeah, I recently upgraded to node 20 and started running with the --loader ts-node/esm parameter but noticed that my memory usage in my application had skyrocketed to 32G before it would die from OOM. I went chasing for a cause because I assumed it was due to a change I made, but downgrading to node 19 lowered memory usage down to around 500M again. I assume this is probably related to the CPU issues @sywesk had.

So I’d recommend people stay off Node 20 until this is fixed if you don’t want to have unexpected issues in your runtime.

everybody here should try https://github.com/privatenumber/tsx. it just works. it can be a drop-in replacement for ts-node

Seeing this on node 18.19.0 as well. Downgrading to 18.18.2 worked for us

Please avoid posting +1s here. Reply if you have something to contribute like a related issue, a fix, or a suggested workaround.

This is also happening to me on GitHub Actions with Node v18, with the following command:

npx --yes ts-node --transpileOnly --esm --experimental-specifier-resolution=node --files ./compile/index.ts

This same command/workflow was working fine a few months ago. Tried downgrading ts-node to 10.9.1 but no luck.

Edit: yeah, I tried tsx and it worked no problem. And btw it’s not referring to using a .tsx extension like I thought at first glance, it’s “TS Execute”, a ts-node alternative. Also, the command ended up being a lot simpler, no flags needed: npx tsx ./compile/index.ts.

Given how fast things change in TS and Node, I feel like it’s not a good sign that this repo is not very active. Not trying to cast blame; seems like it would be a nightmare to stay on top of this. But right now I’d have to recommend tsx and consider ts-node deprecated/abandoned. An issue this big and popular should’ve been patched within a few weeks.

Can’t use esm:

node --loader ts-node/esm ./src/tools/spell-check.ts
(node:18954) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
file:///home/maxim/MyProject/src/tools/spell-check.ts:39
Object.defineProperty(exports, "__esModule", { value: true });
                      ^

ReferenceError: exports is not defined in ES module scope
    at file:///home/maxim/WebWatcher/Frontend/src/tools/spell-check.ts:39:23
    at ModuleJob.run (node:internal/modules/esm/module_job:192:25)

Node.js v20.2.0

One problem with the workarounds is that they hide any typos you have in your code. Like this isn’t very helpful:

$ node --loader ts-node/esm example.ts 

node:internal/process/esm_loader:46
      internalBinding('errors').triggerUncaughtException(
                                ^
[Object: null prototype] {
  [Symbol(nodejs.util.inspect.custom)]: [Function: [nodejs.util.inspect.custom]]
}

Node.js v20.4.0

Yet if you feed the same file into tsc it gives you a much more useful error:

$ npx tsc
example.ts:71:1 - error TS2304: Cannot find name 'make_an_error_happen'.

71 make_an_error_happen
   ~~~~~~~~~~~~~~~~~~~~
Found 1 error in example.ts:71

issue remains unresolved, and is now present in NodeJS v21.0.0 also

My solution was to use vite-node instead.

Awesome this was the easiest workaround, instead of reading all of this useless technical mumbo-jumbo discussion.

The only decent solution I found for this was to switch to tsx btw.

This issue may be a good excuse to try Deno or Bun instead, btw.

For some reason node 18.18.x does work, but node 18.19.x doesn’t. I’m not sure what changed, it’s weird to get a breaking change for this in a minor version.

This seems to be a controversial topic in the node community, but they’re saying it’s because this has to do with the --loader flag, which was marked as experimental, and is therefore not subject to semantic versioning.

If you ask me, anyone still using node without typescript is nuts. So breaking the --loader flag — especially without a clear update path — has a bigger impact than the node folks are acknowledging.

I’m working on a massive codebase that just moved to Node 20 and we’ve had multiple scripts using ts-node end up with the issue described in this thread. The amount of config files required is hard to maintain. Changing one config leads to different errors. I tried tsx and, it just worked…

I don’t understand all the details under the hood of these two packages, but moving everything over to tsx feels attractive at this point.

Just to clear any confusions, tsx mentioned here is TypeScript Execute, not the file extension used in React projects.

For some reason node 18.18.x does work, but node 18.19.x doesn’t. I’m not sure what changed, it’s weird to get a breaking change for this in a minor version.

tsimp is also a neat alternative to ts-node. Works beautifully with newer versions of Node AND maintains strict consistency with TypeScript itself which tsx can’t provide.

https://github.com/tapjs/tsimp

I think everything that needed to be said has already been posted in this thread.

The rest is “me too” for the 2 workarounds and irrelevant messages.

My main takeaway here is that ts-node is becoming unmaintained. Let’s see what path the tools that rely on ts-node take to prevent also becoming irrelevant.

My advice: reconsider the need of ESM modules for your app itfp. None of this helped here for me. But if you are just doing this to be able to have await at top level (my case), consider wrapping it with something like

(async function main() { /**/ })()

which will work perfectly fine in CJS mode, while avoid the need of module type package/target, ESM etc at ll 😃

It’s amazing that people prefer to switch to tsx, instead of getting a small one time pointless warning when they start node: https://github.com/TypeStrong/ts-node/issues/1997#issuecomment-1770364852

I’d like to add quickly that on windows (at least), doing node --loader ts-node/esm ./path works but with an insanely high CPU usage. Doing a quick npx tsc + node ./build/app.js works perfectly without the performance issue.

i expirience the same issue on:

# node --version
v18.16.0

EDIT: my bad, i misinterpreted the gravity of the situation. manually enforcing the loader like this.

{
  "start": "npm run clean && cross-env NODE_ENV=development NODE_OPTIONS=\"--loader=ts-node/esm --trace-warnings\" webpack serve --mode=development --config webpack.config.ts",
}

worked out.

example sourced from here: https://github.com/webpack/webpack-cli/issues/2458

nodejs 版本问题,降低到16.20.x即可

@atomicptr I was able to avoid tsx with the following work around:

package.json

{
    "type": "module",
    "scripts": {
        "start": "node --loader ts-node/esm src/app.ts",
        "test": "c8 --reporter=html node --loader=ts-node/esm node_modules/mocha/lib/cli/cli.js  --grep '' 'tests/**/*.ts'"
    },
    // dependencies
}

I’ve included the testing part as well, because it was a real pain in the ass to setup it up for a project that is ESM only, since jest and nyc don’t work well with ESM only projects but mocha and c8 worked great!

tsconfig.json

{
    "compilerOptions": {
        "target": "es6",
        "module": "NodeNext",
        "outDir": "dist",
        "rootDir": "src",
        "strict": true,
        "esModuleInterop": true,
        "moduleResolution": "NodeNext",
        "sourceMap": true,
        "removeComments": true
    },
    "include": [
        "src"
    ],
    "ts-node": {
        "esm": true
    }
}

and from command like, since I can’t just run ts-node, I had to alias it: alias tn="node --loader ts-node/esm"

I’m relatively new to nodejs and typescript, so if you noticed something weird feel free to suggest improvements.

My solution was to use vite-node instead.

I randomly had an issue with dev server in a pnpm workspace / monorepo with Astro (which uses Vite and is all ESM).

It only cropped up when I tried the setup with turborepo… I have pure pnpm workspaces w/ same project structure running the same node version and had no issues. The app would build but would hang up in dev with unknown file extension “.ts” when trying to import from a local package (ts package with no build of its own). Node 20.11.x. Even if I changed every possible thing including any turborepo boilerplate to be ESM and not CJS it would still happen.

My solution/workaround was to install tsx and change the “dev” script in package.json and have it process the imports.

NODE_OPTIONS='--import=tsx --no-warnings' astro dev

(note: if you run an earlier node 20 you may need different syntax or --loader option)

I honestly have no idea where or how ts-node or anything related to it is getting invoked but this solved the problem for me.

tsx is the best it has been the answer to so many hours of bs troubleshooting across many projects. If something isn’t working with ts-node just use tsx and enjoy things just working like advertised on the box.

There are so many comments here that I thought it may be useful to have a summary:这里有很多评论,我认为总结一下可能会有用:

There are many incompatiblities with Node.js v20.11.0 (current LTS release) and ts-node v10.9.2 (current release). It comes down to the following:与Node.js v20.11.0(当前LTS版本)和ts-node v10.9.2(当前版本)有许多不兼容之处。它归结为以下几点:

When using Node.js 18 and CommonJS, you can use the following start scripts:使用Node.js 18和CommonJS时,可以使用以下启动脚本:

  • ts-node src/main.ts
  • node --require ts-node/register src/main.ts

When using Node.js 18 and ESM, you can use:使用Node.js 18和ESM时,您可以使用:用途:

  • ts-node-esm src/main.ts
  • node --loader ts-node/esm src/main.ts
  • node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts (if you don’t want to see experimental warnings) node --no-warnings=ExperimentalWarning --loader ts-node/esm src/main.ts (如果您不想看到实验警告)

When using Node.js 20 and ESM, you cannot use ts-node-esm anymore (it will show you ERR_UNKNOWN_FILE_EXTENSION). You have to use:当使用Node.js 20和ESM时,你不能再使用了(它会显示给你)。您必须使用:用途:

  • node --loader ts-node/esm src/main.ts

Unfortunately, this won’t give you stacktraces anymore. So if there is a compiler error, you will only get to see:不幸的是,这不会再给你stacktrace了。所以如果有编译器错误,你只能看到:

internalBinding(‘errors’).triggerUncaughtExceptionInternalBinding(‘errors’).triggerUncaughtException

The TypeScript runner tsx won’t help in this situation as it doesn’t provide type checking capabilities.类型脚本运行器TSX在这种情况下帮不上忙,因为它不提供类型检查功能。

You can work around this by using a combination of tsc and ts-node such as:您可以通过组合使用 tscts-node 来解决此问题,例如:

  • tsc --noEmit && node --loader ts-node/esm src/main.ts

Alternatively, you can use tsimp:或者,您可以使用tsimp:

  • TSIMP_DIAG=error node --import=tsimp/import src/main.ts

I also made a tutorial showing each step:我还制作了一个教程,展示了每一步:

I used this method and eventually my project worked successfully in Node.js 20.

But what is confusing is that if I try to use ts-node-esm, then it will choose to use the globally installed ts-node, and then it will report an error “ERR_UNKNOWN_FILE_EXTENSION”. If using node and specifying loader, the globally installed ts-node won’t work again, it will report “ERR_MODULE_NOT_FOUND”, and ts-node has to be installed in the project to work properly.

@1C0D Grow up.

@pencilcheck can you give me an example? I’m currently working on a large project that is ESM only, and haven’t seen any unexpected issues.

With so many recommendations of tsx, I’d like to remind that tsx simply ignores all the type information.

Just like ts-node in SWC mode, which which is preferred for development anyway. Type-checking can still be in CI and in the build process.

The following workaround I found works without warnings (but needs Node >=20):

  1. Create a new file register-hooks.js:
import url from 'node:url'
import { register } from 'node:module'

const __filename = url.fileURLToPath(import.meta.url)
register('ts-node/esm', url.pathToFileURL(__filename))
  1. Use the following command:
node --import ./register-hooks.js ./src

If you’re using imports without file extensions, you need to add --experimental-specifier-resolution=node:

node --experimental-specifier-resolution=node --import ./register-hooks.js ./src

Otherwise the following workaround works on Node >=20 and Node 18:

node --experimental-loader=ts-node/esm  ./src

Yes, node --no-warnings=ExperimentalWarning --loader ts-node/esm file.ts helps to bypass the issue until it’s fixed. @RobinTail

Note — by using --no-warnings=ExperimentalWarning you’re suppressing all node warnings because --no-warnings flag does not handle a value. --no-warnings works the same. https://nodejs.org/api/cli.html#--no-warnings

You can do with just node. Instead of node, I’m doing this:

node --no-warnings=ExperimentalWarning --loader ts-node/esm

On the other hand, tsx lacks type checking. tsimp may be a better drop-in replacement than tsx.

Use node --loader ts-node/esm to replace ts-node-esm as a temporary workaround. I hope ts-node or node could fix this issue in future.

still facing this issue

node v21.5.0 
ts-node 10.9.2
typescript 5.3.3

It’s amazing that people prefer to switch to tsx, instead of getting a small one time pointless warning when they start node: #1997 (comment)

it’s because tsconfig.json and ts-node simply doesn’t work. You can follow all the instructions, it will work perhaps once you isolated everything but in real projects, it simply stops working due to all other unknown factors.

For anyone facing a issue with getting node v19 to run your ts node server again.

I was facing the same issue after downloading a new package and couldn’t figure out why all of a sudden my ts node server wouldn’t start. I had first thought that it had something to do with upgrading to node v20 from v19. However, downgrading back to node v19 still didn’t fix the issue with “Unknown file extension .ts” error. I had read online that maybe it had someting to do my with tsconfig.ts file. However, it was working perfectly fine with node v19. I was about to go the tsx route - https://www.npmjs.com/package/tsx until I tried running the cmd npx tsx app.ts and the terminal started that something was already running on port 3000. I then realize that I had accidentically open another terminal and tried running my app again, which is why I got the error “Unkwon file extension .ts”. Here’s my tsconfig.ts file.

{
  "compilerOptions": {
    "target": "es2020", 
    "module": "NodeNext",   
    "moduleResolution": "NodeNext",      
    "sourceMap": true,
    "outDir": "./build",
    "strict": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true, 
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
  },
  "include": ["src/", "app.ts", "public/html/fileUpload.js"],
  "exclude": ["node_modules", "**/*.spec.ts"],
  "ts-node": {
    "esm": true
  }
}

Here’s my package.json file

{
 "main": "app",
  "type": "module",
  "scripts": {
      "dev": "nodemon app.ts",
      "start": "node build/app.js,
      "build": "tsc"
    }.
 "dependencies": {
    "express": "^4.18.2"
   },
  "devDependencies": {
      "nodemon": "^3.0.1",
      "typescript": "^5.3.2"
    },
 "engines": {
    "npm": ">=9.0.0",
    "node": "18.17.0"
  }
}

To run app type - npm run dev

How I import file

Ex. import auth from “src/auth/auth.js”

Create a file in root directory name .nmprc to force the use of correct nodejs version. Be sure to add the property in file.

engine-strict=true

Here’s my file structure

  • Create a file in root directory as app.ts. Then create a folder in root directory with name src/app/. In src/app I have create d more .ts files and create subfolder that contain .ts files. REmember to import .ts with the .js extension.

Edit - Since I have been downvoted with no explaination. I asume it was bc what I have stated is not working. I have added the engine field to package.json. This specifics the nodejs version that works for me without throwing the .ts config error and that works with the nodejs package sharp for image processing in my project. Not sure about all the details but I do know that nodemon does everything and more that ts-node would do. I use nodemon in dev to get auto recompile. I also added the start script and build script to my package.json file. In production your cloud server hoster should have in thier build process to run the build script in the package.json file using the command - npm run build and then to start the application with the comand - npm run start. If your wondering why the path is build/app in package.json. It’s bc in the tsconfig file there’s property named outDir that specified the output folder to build.

I Hope the further explainaiton helps. Happy new years everyone!🎉

Switching to TSX worked out of the box, so will be switching to that.

With so many recommendations of tsx, I’d like to remind that tsx simply ignores all the type information.

I made a library to execute projects with path-mapping: @bleed-believer/path-alias and works with node 20. This library uses ts-node as dependency, and uses ts-node loaders only when your execution calls a file inside of your source files. If you want to try:

  • Install the package:

    npm i --save @bleed-believer/path-alias
    
  • Assuming your source code is inside ./src folder, simply execute (this will run using ts-node hooks):

    # If you want to use the integrated cli
    npx bb-path-alias ./src/index.ts
    
    # Or if you want to use the --import flag instead
    node --import @bleed-believer/path-alias ./src/index.ts
    
  • Assuming your transpiled code is inside ./dist folder, simply execute (this will not use the ts-node hooks):

    # If you want to use the integrated cli
    npx bb-path-alias ./dist/index.js
    
    # Or if you want to use the --import flag instead
    node --import @bleed-believer/path-alias ./dist/index.js
    

I’ve been using bun (https://bun.sh) lately to replace ts-node and tsx, and it seems to be working quite well for my purposes. Maybe it can help some folks here.

I tried out tsx to see if it also has issues with Node v20 and it does.

Node v20 line numbers seem to be based on single-line transpiled code:

$ node --loader tsx --no-warnings --test ./api/src/shared/socket-set.test.ts:
  TypeError [Error]: Cannot read properties of undefined (reading 'once')
      at SocketSet.add (file:///Users/blah/blah/api/src/shared/socket-set.ts:1:970)

Node v18 produces correct line numbers:

$ node --loader tsx --no-warnings --test ./api/src/shared/socket-set.test.ts:
  TypeError [Error]: Cannot read properties of undefined (reading 'once')
     at SocketSet.add (file:///Users/blah/blah/api/src/shared/socket-set.ts:62:14)

In the end I went with tsx, 0 config and having a separate typecheck command is what I was used to before so not a big deal

tsx also offers no support for emitDecoratorMetadata, which doesn’t bide well for any libraries that need reflect-metadata (e.g. TypeORM).

Regardless of preferences, ts-node type checks by default and therefore tsimp is a better drop-in replacement to ts-node than tsx since it behaves the same as ts-node by default (I’m not taking into account when used with swc).

If you don’t need type checking, then go with tsx of course.

But, for example, my use case needs type checking when executing the application.

Can anyone summarize when this problem will be fixed in the ts-node?

for NodeJS 20 this comment helped me: https://github.com/TypeStrong/ts-node/issues/1997#issuecomment-1537111441

But any error logs are still shown as:

... [Object: null prototype] ...

And this makes development impossible

@vasily-mishanin we can. 🙂 If you bait for an answer, here it is: you need to use --no-warnings=ExperimentalWarning --loader node_modules/ts-node/esm.mjs index.ts

In my case I use spawnSync to pass the execution down ts-node:

#! /usr/bin/env node
…
spawn(process.execPath, [ '--no-warnings=ExperimentalWarning', '--loader', path_loader, path_bin, ...argv ], { stdio: 'inherit' })

It is ordinary executable from outside, the package is type=module and I use TS for all the code.

@1C0D And you are the expert apparently, using tsx when you don’t have jsx, right?

I also get this error when using node v18.19.0, but works fine on v18.18.2

Yeah, this could be the same change affecting it: https://nodejs.org/en/blog/release/v18.19.0

Loaders now apply to subsequent loaders, for example: --experimental-loader ts-node --experimental-loader loader-written-in-typescript.

Also for whoever finds this useful, you can use this as a shebang too for scripts in the package.json bin field:

#!/usr/bin/env node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm

I also have { "type": "module" } although I’m not 100% if it’s needed.

Worth noting that using node ... --loader approach described above, the ts-node config in tsconfig.json isn’t respected so you need to specify everything as flags to node.

I encountered this issue when trying to run mocha tests with native ES modules, after reviewing a lot of suggestions above, searching a lot of web pages, and doing a lot of try-outs, I finally got it worked.

Here is what I do:

  1. set these compiler options options in tsconfig.json
{
    "compilerOptions": {
        "module": "esnext", // of course
        "noEmit": true,
        "allowImportingTsExtensions": true, // This is optional, but since I use this style, I need to turn it on.
        "esModuleInterop": true, //This is required since some of the dependencies are CommonJS packages.
        "moduleResolution": "NodeNext", // required
        // ...
    }
}
  1. set type: module in package.json (this is the key setting), since I’m coding a dual module system package, I wrote two scripts to temporarily change this setting during the test.
// pretest.cjs
const fs = require("fs");
const pkg = require("./package.json");

pkg.type = "module";

fs.writeFileSync("./package.json", JSON.stringify(pkg, null, "    ") + "\n", "utf8");
// posttest.cjs
const fs = require("fs");
const pkg = require("./package.json");

delete pkg.type;

fs.writeFileSync("./package.json", JSON.stringify(pkg, null, "    "), "utf8");
  1. Change the test command to this:
{
    "script": {
        "pretest": "node pretest.cjs",
        "posttest": "node posttest.cjs",
        // use `node --loader=ts-node/esm mocha` in stead of `mocha -r ts-node/esm`
        "test": "node --no-warnings=ExperimentalWarning --loader=ts-node/esm ./node_modules/mocha/bin/mocha *.test.ts"
    }
}

This configuration allows me to test from Node.js v14 (TypeScript 5.x demands) to v20.

Hope this helps others, especially for doing mocha tests.

Just chiming in here regarding the OOM issue: I believe it’s FUD today and you’re safe to update and use the workarounds detailed in this thread. The OOM issue with node on macOS was fixed in 20.1.0, see https://github.com/nodejs/node/issues/47761#issuecomment-1533793099

I got my “npm run start” script to work with nodemon this way if this helps anyone:

“start”: “nodemon --exec node --no-warnings=ExperimentalWarning --loader ts-node/esm src/server.ts”

Replace “src/server.ts” with the path to the file you want to execute.

tsimp has typechecking by default. I believe it’s the best alternative considering compatibility.

Node.js v20.6.1

I am still looking for solutions.

I was using Module type overrides to force for ESM given I can’t set type: module in package.json (and prefer to avoid using mts extension*

* given some tools don’t automatically recognize as Typescript files. Like Prettier plugin for WebStorm

And workarounds provided don’t allow those overrides to work.

Eventually, switched to tsm. No need to use mts extension (as when using module overrides). Worked like a charm 🎉

For a specific script, I had to use tsx as @uriva suggested. Plus set the extension of the file to run (which was ESM) to mts. Probably because it’s importing CJS modules at some point. Not sure TBH

@Maxim-Mazurok, @dandv Are you sure you flagged your files as ESM properly? This can either be done by renaming the files to .mts or by having the package.json file corresponding to the source file containing "type": "module"

I use CJS, and have type: commonjs in my package.json.

The following ended up working for me: tsconfig.node.json:

{
  "extends": "@tsconfig/node18/tsconfig.json",
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "playwright.config.*",
    "orval.config.*",
    "src/tools/**/*"
  ],
  "compilerOptions": {
    "composite": true,
    "types": ["node"]
  }
}

(inherited “module”: “Node16”, “target”: “es2022”)

package.json scripts:

"ts-node-tool": "cross-env TS_NODE_PROJECT=\"tsconfig.node.json\" ts-node",
"my-tool": "npm run ts-node-tool ./src/tools/my-tool.ts"

Of course, can’t use top-level await and had to use tsimportlib to import esm deps, might find more on that in https://github.com/Maxim-Mazurok/esm-in-cjs-ts-demo

Unsubscribing for now as I’m no longer blocked and happy with a solution. Hope it helps someone.

I’d recommend tsx as alternative, @phosmium && @PrimalZed . In my experience it works faster than vite-node and it has flawless ESM support right out of the box without any crutches comparing to swc-node.

We had been using ts-node with the --swc option when we ran into the issue on v18.19.0 from our nightly base image build. We were able to use @swc-node/register to use swc directly to get over this issue:

node -r @swc-node/register ./tools/our-app.ts

Just added a reference to @swc-node/register, removed ts-node, and removed the tsconfig.json (it worked with the default configuration).

It is a pity we can not use ts-node with .ts files in “type”: “module” projects

Not to forget that your IDE does type checking as well against your tsconfig. And anyone who uses ts-node with the --swc flag has the same “problem”. Putting that in quotes since it is not really a problem at all. Your build pipeline will sitll use tsc in some way or another to check types, but hot reloads are so much faster in development.

It’s amazing that people prefer to switch to tsx, instead of getting a small one time pointless warning when they start node: #1997 (comment)

@eldare Just to point out, a lot of people in this thread report high memory and cpu usage too. So I think that perhaps you might be missing something with the “pointless” warning.

I would personally have to get a fairly large concussion before I ever considered to run ts-node in prod. But for me a slow dev experience is undesirable, especially as jest requires ts-jest and ts-node, and running it is hella slow since Node 20 and hard to say with confidence what is the slow component in this mess.

@1C0D Grow up.

@pencilcheck can you give me an example? I’m currently working on a large project that is ESM only, and haven’t seen any unexpected issues.

I have a project right now, that have the two .ts files, one is executed with ts-node loader, and it seems to work, the other I wrote that imports “lodash” and that’s it.

One without lodash works, the other with lodash simply error out. Now how do I make sure this works? to support both commonjs imports and esm imports with the subtlety of picking whether to transform the import or the files, or picking the right format at the file in node_modules is just impossible for me to understand.

All I have is to change either “type”: “module” in package.json, (that will break stuff), or change tsconfig to include module, target, and compileOptions.

None of the changes I have will fix this, because tsconfig is just confusing and hard to isolate. (btw, while writing this message, I just switch to tsx, and it just works, no confusing error, but actual error in my code that I can use to fix and start running)

This is just one of an example. I have another project with expo, and I have a sever component. I’m using telefunc so I can unify server and expo code into one repo.

Expo uses a completely different bundler and compiler system than a normal typescript import system. If you setup the tsconfig for expo, you will not be able to make it work with server code that uses normal server code in ts.

Look, i’m not expert in node, I just use node so I can get on with my work. tsx just works, so I will continue to use tsx.

It’s amazing that people prefer to switch to tsx, instead of getting a small one time pointless warning when they start node: https://github.com/TypeStrong/ts-node/issues/1997#issuecomment-1770364852

Well, that was the first thing I tried. But it didn’t work.

works for me:

node --no-warnings=ExperimentalWarning --experimental-specifier-resolution=node --loader ts-node/esm [file]

this made ts-node unusable for me for a long time, I moved to tsx, it was easy and works well.

@nickmccurdy Why do you think that? It doesn’t print a warning for me with swc-node. Also I think it’s a new feature, would be weird if it was deprecated already

daniel@mmmmmmmmmm test2 % yarn node --loader @swc-node/register/esm test.ts
hi

It’s not exactly deprecated. But looking at this, it looks like the Node.js team discourages devs to use this flag: https://nodejs.org/api/cli.html#--experimental-loadermodule

This issue is open for half a year now and I wonder what is its status? We have Node 21 now and the issue is still reproducible with ts-node 11 beta. The workaround of --loader ts-node/esm results in an ugly warning that cannot be silenced, except together with all Node warnings. tsx is not an option for me as it doesn’t perform any type-checking.

There’s a much better (no warnings) and more performant drop-in alternative IMO, which is: https://github.com/swc-project/swc-node

To use: yarn add @swc-node/register @swc/core

And then to run typescript just do node --loader @swc-node/register/esm index.ts

This issue is open for half a year now and I wonder what is its status? We have Node 21 now and the issue is still reproducible with ts-node 11 beta. The workaround of --loader ts-node/esm results in an ugly warning that cannot be silenced, except together with all Node warnings. tsx is not an option for me as it doesn’t perform any type-checking.