Repository navigation
ts-node doesn't run ESM modules as expected, either refusingimport statements in ts file or not being able to run .ts files #2086
Description
Search Terms
ESM import paths
Expected Behavior
I expect ts-node to be able to run typescript code using ESM module imports. However , it either fails with
SyntaxError: Cannot use import statement outside a module
or after adding a "type": "module" to package.json, fails with
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/MY_PROJECT_DIR/workers/master.ts
The documentation is rather insufficient on how ts-node operations with tsconfig.json and the type value in package.json.
Of all of target, module and moduleResolution in tsconfig.json:compilerOptions, what should be the first value that is set (as in, does target determine module and moduleResolution's values, or vice versa ?)
Please provide some guidance on how this works.
Actual Behavior
Steps to reproduce the problem
This file creates basically parses the env variables and then creates a bunch of worker processes.
run with npx ts-node workers/master.ts
// workers/master.ts
import Redis from 'ioredis';
import { fork } from 'child_process';
import dotenv from 'dotenv';
import { Worker } from './worker';
import { isValid } from './../shared/lib/utils';
dotenv.config();
const streamNames: string[] = ["stream-1", "stream-2"];
const numWorkersPerStream: number = 5;
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = isValid(process.env.REDIS_PORT) ? parseInt(process.env.REDIS_PORT!, 10) : null;
if (!isValid(REDIS_HOST)) {
console.error('REDIS_HOST is not a valid environmental variable');
process.exit(1);
}
if (!isValid(REDIS_PORT)) {
console.error('REDIS_PORT is not a valid environmental variable');
process.exit(1);
}
const redisClient = new Redis({
host: REDIS_HOST!,
port: REDIS_PORT!,
});
async function createConsumerGroupIfNotExists(streamName: string, consumerGroup: string) {
try {
await redisClient.xgroup('CREATE', streamName, consumerGroup, '$', 'MKSTREAM');
console.log(`Consumer group "${consumerGroup}" created (if not already exists) for stream "${streamName}"`);
} catch (error: any) {
if (!error.message.includes('BUSYGROUP Consumer Group name already exists')) {
console.error('Error creating consumer group:', error);
}
}
}
// Create consumer groups only once
for (let i = 0; i < streamNames.length; i++) {
const streamName = streamNames[i];
const consumerGroup = `${streamName}-group`;
createConsumerGroupIfNotExists(streamName, consumerGroup);
}
// Create worker processes for each stream and group combination
for (let i = 0; i < streamNames.length; i++) {
for (let j = 0; j < numWorkersPerStream; j++) {
const workerName: string = `worker-${streamNames[i]}-${j}`;
const streamName: string = streamNames[i];
const consumerGroup: string = `${streamName}-group`;
const args: string[] = [workerName, streamName, consumerGroup, REDIS_HOST!, REDIS_PORT!.toString()];
const workerProcess = fork("./workers/worker.ts", args);
workerProcess.send("start"); // Signal the child process to start
}
}%
Minimal reproduction
Specifications
OS: macOS Sonoma 14.1.1
"ts-node": "^10.9.1"
node version: 20.6.1
tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"ts-node": {
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"moduleResolution": "nodenext"
}
}
}
Activity
MGMehdi commentedon Nov 21, 2023
I've got the same issue.
I've tried to change the
targetandmodulein tsconfig.json and addtype:modulein package.json. Tried every combinaison but there is always something that prevents to run.It looks like this problem has been around for a few years from the posts on github and stackoverlow.
nikkehtine commentedon Nov 21, 2023
When I run
ts-node-esm ./src/index.tsorts-node --esm ./src/index.ts, I get this error:tsconfig:
AitorMelero commentedon Nov 22, 2023
I had the same issue on a computer with macOS, another with Windows, and another with Ubuntu, all of them running Node version 20. When testing the previous version of Node (18.18.2) on a machine with Windows and another with Ubuntu, it worked on both.
It appears that the issue arises when using version 10 of ts-node with Node version 20.
ethanhalko commentedon Nov 22, 2023
I was finally able to get this working with Node 20 and ts-node 10 by setting
moduletocommonjsandtargettoes6.Provided your project can support your Typescript compiling to CommonJS, it works well.
arbassic commentedon Nov 22, 2023
In the latest node.js weekly newsletter they sent an interesting gist / project stub addressing the issues - it may be helpful!
https://gist.github.com/khalidx/1c670478427cc0691bda00a80208c8cc
It is really weird that ts-node in conjuction with Node 20 is making such problems!
Also.. have you tried tsx?
janpapenbrock commentedon Nov 25, 2023
This is probably a duplicate of #1997?
Zen-cronic commentedon May 11, 2024
Hey, I had the same issue for my use case that resembles your "Steps to reproduce problem".
My case: Configuring
ts-jestfor my tests. Andjestusests-nodeto compile thetypescripttest files. In those files, I am usingforkfromchild_process(like in your code) to create processes and execute functions. But it was throwing this errorby pointing at the ESM
importstatement declared in those processes.Solution: register ts-node in the environments where the processes are executed.
My setup:
package.jsontype: moduleis NOT included. Just CommonJS module.tsconfig.jsonmoduleset tocommonjsandtargetset ases2020.These two properties doesn't have to be the same: the former is responsible for the type of import statements; the latter modifies the compiled JS version.
source file:testHelper.tsThere are 2 approaches you can take:
A. Set the args to the child proc as
-r ts-node/registerwhich tells the node environment of the proc to be compiled with ts-node, because the proc doesn't know that by default.B. Add those arguments in the test script in
package.jsondepending on the platform. For linux, useexport NODE_OPTIONS='-r ts-node/register'Solutions found from this thread #619 and the docs.
Hope that helps!
crazyjat commentedon Oct 21, 2024
Why are we still doing commonjs? Is ESM not the standard at this point?
jishi commentedon Nov 6, 2024
I came across this very issue a few days ago, but it was incoherent over different repositories. I finally concluded that this error started with Typescript 5.6.x, and doesn't happen in 5.5.x and below (this is without any tsconfig.json in place, which I expect should work out of the box).
Not sure what changed in typescript 5.6 which makes the inferred tsconfig to start failing like this (this happens in both Node 18 and Node 20 btw, which I know have other issues).
aaarichter commentedon Nov 22, 2024
having the same problem. The solutions from the readme doesn't work :(
brunocapdevila commentedon Dec 16, 2024
Same issue on Mac with node 20.15.1
Ziv-Barber commentedon Jan 8, 2025
Same issue in 2025. Still no solution.
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
11 remaining items
Use NODE_OPTIONS=--no-experimental-strip-types for node 22.18
Use NODE_OPTIONS=--no-experimental-strip-types for node 22.18
Use NODE_OPTIONS=--no-experimental-strip-types for node 22.18
Use NODE_OPTIONS=--no-experimental-strip-types for node 22.18
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
feat: move to tsx (#194)
MauricioEF commentedon Apr 22, 2026
Still no solution on 2026, you should totally deprecate ts-node if you're not about making this work as default for esm