Skip to content

ts-node doesn't run ESM modules as expected, either refusingimport statements in ts file or not being able to run .ts files #2086

Open

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

  1. MGMehdi commented on Nov 21, 2023

    @MGMehdi

    I've got the same issue.
    I've tried to change the target and module in tsconfig.json and add type:module in 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.

  2. nikkehtine commented on Nov 21, 2023

    @nikkehtine

    When I run ts-node-esm ./src/index.ts or ts-node --esm ./src/index.ts, I get this error:

    TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for D:\Code\simple_node_server\src\index.ts
        at new NodeError (node:internal/errors:406:5)
        at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:99:9)
        at defaultGetFormat (node:internal/modules/esm/get_format:142:36)
        at defaultLoad (node:internal/modules/esm/load:120:20)
        at nextLoad (node:internal/modules/esm/hooks:833:28)
        at load (D:\Biblioteka\pnpm-store\5\.pnpm\ts-node@10.9.1_@types+node@14.18.33_typescript@5.2.2\node_modules\ts-node\dist\child\child-loader.js:19:122)
        at nextLoad (node:internal/modules/esm/hooks:833:28)
        at Hooks.load (node:internal/modules/esm/hooks:416:26)
        at MessagePort.handleMessage (node:internal/modules/esm/worker:168:24)
        at [nodejs.internal.kHybridDispatch] (node:internal/event_target:807:20) {      
      code: 'ERR_UNKNOWN_FILE_EXTENSION'
    }
    

    tsconfig:

    "compilerOptions": {
        "target": "es2016",
        "module": "esnext",
        "esModuleInterop": true,
      }
  3. AitorMelero commented on Nov 22, 2023

    @AitorMelero

    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.

  4. ethanhalko commented on Nov 22, 2023

    @ethanhalko

    I was finally able to get this working with Node 20 and ts-node 10 by setting module to commonjs and target to es6.

    Provided your project can support your Typescript compiling to CommonJS, it works well.

  5. arbassic commented on Nov 22, 2023

    @arbassic

    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?

  6. janpapenbrock commented on Nov 25, 2023

    @janpapenbrock

    This is probably a duplicate of #1997?

  7. Zen-cronic commented on May 11, 2024

    @Zen-cronic

    Hey, I had the same issue for my use case that resembles your "Steps to reproduce problem".

    My case: Configuring ts-jest for my tests. And jest uses ts-node to compile the typescript test files. In those files, I am using fork from child_process (like in your code) to create processes and execute functions. But it was throwing this error

    SyntaxError: Cannot use import statement outside a module 

    by pointing at the ESM import statement declared in those processes.

    Solution: register ts-node in the environments where the processes are executed.

    My setup:

    1. package.json

    type: module is NOT included. Just CommonJS module.

    1. tsconfig.json

    module set to commonjs and target set as es2020.
    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.

    {
      "compileOnSave": true,
      "compilerOptions": {
        "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
         "module": "commonjs" /* Specify what module code is generated. */,
        "rootDir": "./src" /* Specify the root folder within your source files. */,
         "outDir": "./dist" /* Specify an output folder for all emitted files. */,
        "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
        "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
        "strict": true /* Enable all strict type-checking options. */,
        "skipLibCheck": true /* Skip type checking all .d.ts files. */
      },
      "exclude": ["node_modules", ...]
    }
    1. source file: testHelper.ts

    There are 2 approaches you can take:

    A. Set the args to the child proc as -r ts-node/register which tells the node environment of the proc to be compiled with ts-node, because the proc doesn't know that by default.

        workerProcess = fork(testProcessPath, {
          stdio: [0, "pipe", "pipe", "ipc"],
          execArgv: ["-r", "ts-node/register"],
        ...
        });

    B. Add those arguments in the test script in package.json depending on the platform. For linux, use export NODE_OPTIONS='-r ts-node/register'

    "scripts": {
       ...
        "test": "set NODE_OPTIONS=-r ts-node/register && jest --watchAll --no-cache",
      },

    Solutions found from this thread #619 and the docs.

    Hope that helps!

  8. crazyjat commented on Oct 21, 2024

    @crazyjat

    Why are we still doing commonjs? Is ESM not the standard at this point?

  9. jishi commented on Nov 6, 2024

    @jishi

    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).

  10. aaarichter commented on Nov 22, 2024

    @aaarichter

    having the same problem. The solutions from the readme doesn't work :(

  11. brunocapdevila commented on Dec 16, 2024

    @brunocapdevila

    Same issue on Mac with node 20.15.1

  12. Ziv-Barber commented on Jan 8, 2025

    @Ziv-Barber

    Same issue in 2025. Still no solution.

  13. added a commit that references this issue on Jan 11, 2025
  14. added a commit that references this issue on Jan 25, 2025
  15. added a commit that references this issue on Feb 19, 2025
  16. 11 remaining items

  17. added a commit that references this issue on Aug 31, 2025
  18. added a commit that references this issue on Sep 16, 2025
  19. added a commit that references this issue on Sep 25, 2025
  20. added a commit that references this issue on Sep 25, 2025
  21. added a commit that references this issue on Sep 28, 2025
  22. added a commit that references this issue on Sep 29, 2025
  23. added a commit that references this issue on Sep 29, 2025
  24. added a commit that references this issue on Sep 30, 2025
  25. MauricioEF commented on Apr 22, 2026

    @MauricioEF

    Still no solution on 2026, you should totally deprecate ts-node if you're not about making this work as default for esm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions