Unable to import ESM .ts module in node
Asked Answered
H

4

30

I have been trying to import an ESM module written in typescript in nodejs. But I am getting the following error:

An import path cannot end with a '.ts' extension.

Util.ts

 export class Util {
    constructor ( ) {
       
    }
      log(msg) {
        console.log(msg) 
    }
  }

index.ts

import {log} from './Util.ts'
log(task.query.criteria, payload.parameters)

I have also added "type":"module" inside package.json

I changes .ts to .js just to see if it works and then I got :

Object.defineProperty(exports, "__esModule", { value: true });                         ^

ReferenceError: exports is not defined
at file:///C:/Users/abc/NestJsPOC/NestPOC/dist/main.js:2:23

tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true
  }
}

EDIT

I have also tried:

 var log = require('../utility/util.js');

Util.js

    function log(msg) {
      console.log(msg)
     
  }
    module.exports= { log}

index.ts

    log('hello')

Error:

TypeError: log is not a function
Hauge answered 4/9, 2020 at 14:9 Comment(10)
Simply remove the .js from import. If you want the extension see this. You can change from Node JS to Deno that support Typescript by default and use the .ts import path. Use Deno only if you know what are you doing since is relative new.Novice
@CarloCorradini sorry that was a mistake while copy pasting.. I have tried all. ".js", ".ts" and also removing all extensions. I believe extension is mandatory while importing ESM mdoules.Hauge
In tsconfig.json (create one if not present) in compilerOptions add the following line: "module": "commonjs". Typescript info page here. Tsconfig info page here.Novice
That's how it already is. I have added tsconfig in my post.Hauge
I created a simple example here. You are trying to call the function log without using the class Util. More documentation here.Novice
does it have to be static?Hauge
From Static Properties: We can also create static members of a class, those that are visible on the class itself rather than on the instances. In summary, you don't have to create an instance of the class using the new keyword to call the function.Novice
I suggest you to read the book Programming TypeScript: Making Your JavaScript Applications Scale. It's a solid reference for the Typescript world.Novice
@Hauge in NodeJS extensions are mandatory in TypeScript not. To enable that same behaviour in NodeJS you need to use the flag --experimental-specifier-resolution=node for more details check my answer below.Pianist
@Hauge does my answer helped?Pianist
C
82

I've tried every solution I can find or think of multiple times over the past several years and I've just found the following package which worked out of the box without needing any additional setup:

https://www.npmjs.com/package/tsx

It just works:

npm i -D tsx
npx tsx src/index.ts
Clinician answered 29/11, 2022 at 1:18 Comment(8)
This got me back up and running whereas the most-upvoted answer I could not get it working with ts-node. Your answer worked great.Cement
HOLY CRAP... tsx works instantly?! Interop is so much better... blown away to have found this. Thank you!Botanical
Sadly tsx doesn't support decorators yet. /:Ovine
Wow. This should be the answer OP!!!! I hate ts-node now...Damales
Thank you for this! I struggled much until I found this answer. I agree, I hate ts-node nowGlop
this is 100% the correct answer.Faulty
The most voted answer was answered in 2020 and updated in 2022. At that time tsx was not popular. I’ll carve some time to update my repo and answer with this option.Pianist
The best answer ever!! No decorator is fine. At least it doesn't waste more time than it should be.Intellectualism
P
76

Updated on 14.09.2022

I've created a repository with the necessary settings to run esm directly via ts-node or using tsc and running the transpiled esm file with NodeJS. https://github.com/felipeplets/esm-examples

Keep in mind that you need to use ts-node 10+ and NodeJS 12+ in order to use the setup below.

The updated settings to make it by the time I'm answering your question (updated on 29.05.2022) is:

Original answer

It seems you are looking to use ESM with Node and TS.

tsconfig.json (tested with TypeScript 4.7 and 4.8)

On your tsconfig.json file make sure to have the following settings:

{
    "compilerOptions": {
        "lib": ["ES2022"], // ES2020 in earlier versions
        "module": "ES2022", //ESNext 
        "moduleResolution": "Node",
        "target": "ES2022", // ES2020 in earlier versions
        "esModuleInterop": true,
        ...
    },
    ... 
}

package.json

Make sure to have the following attribute on your package.json file.

{ 
    ...
    "type":"module",
    ...
}

Running it with transpiled files

You can't import .ts files direct in NodeJS, you need to first transpile it using tsc to then import it in the NodeJS in runtime, so in the end you will be importing the .js and not the .ts files. (To run it as .ts please make sure to read the next section of my answer. Running it with ts-node)

When running it you need to use the flag --experimental-specifier-resolution=node as it will enable you to have imports with no extensions as it happens in TypeScript:

node --experimental-specifier-resolution=node index

Note that the flag order matters — you must put it before the file path.

You can find more details about the flag on https://nodejs.org/api/esm.html#esm_customizing_esm_specifier_resolution_algorithm

Running it with ts-node

ts-node is a runtime transpiler so it allow you to import typescript files in runtime when using node.

There is a feature in ts-node that allows you to run it using ESM, to use it you will need to install TS Node 9.1+. For more details on the implementation and possible issues check: https://github.com/TypeStrong/ts-node/issues/1007

To install it you will need to run:

npm i -D ts-node 

And to run the service supporting the new resolution mode and the TSNode loader you will need to run:

ts-node 10.8+

You can add esm and experimentalSpecifierResolution in your tsconfig file.

{
    ...
    "ts-node": {
        "esm": true,
        "experimentalSpecifierResolution": "node"
    }
}

Now you can simply run:

ts-node index

Alternatively you could set ts-node --esm or ts-node-esm to achieve the same without setting it on the the tsconfig.json file.

ts-node up to 10.7

node --experimental-specifier-resolution=node --loader ts-node/esm index.ts

After this you will be able to use TS and ESM in your project in runtime and this statement will be possible:

import { helloWorld } from './Util'

Where helloWorld is an exported member from Util.ts

Important! I don't recommend using ts-node in production, so this is very handy and useful for a development environment but make sure to transpile and use the resulting code in production to avoid possible memory issues with TS Node.

Pianist answered 5/12, 2020 at 23:30 Comment(16)
working for me, with ts-node 10.7.0 in a docker environment...without --experimental-specifier-resolution=node ts-node wanted a .js file extensionAsyut
"--experimental-specifier-resolution=node" helped me - thanks! Eh ESM is such a tragedy for node.js development.Ratel
This isn't helping me. No matter what I do, I can't get beyond Unknown file extension ".ts" when I try to run my code using node --experimental-specifier-resolution=node --loader ts-node/esm app.ts.Mcmurray
@Mcmurray can you share a repo on GitHub reproducing the issue?Pianist
@Felipe Plets, sure: github.com/kshetline/geo-db-updater/tree/….Mcmurray
@Mcmurray the path was missing "src/" before the file namw. I've opened a PR shoing you how to fix it: github.com/kshetline/geo-db-updater/pull/1Pianist
@Felipe Plets, weird. That didn't help for me. I had tried that before, and also ./src/app.ts. I was running from my IDE (Intellij IDEA) before, so I thought I'd see if running from a terminal using npm start would make a difference. It did, but not a helpful difference. Just a different error, complaining about an import being a CommonJS module. I tried this used Node.js 14, 16, and 18. I'm sure you got it to work, so it's very mysterious why I'm still having a problem.Mcmurray
for use with ts-node, you can now run ts-node --esm index.ts or ts-node-esm index.ts: github.com/TypeStrong/ts-node#esmOverlive
Thank you @Overlive for the information. I have updated my answer and provided a repository with the latest configuration.Pianist
Thank you for this well-written answer! I'm afraid your suggestion regarding relative imports (import { log } from 'Util' still requires the --experimental-specifier-resolution=node flag for ts-node(!), I opened a PR against your repo to demonstrate it. At least, it's working with this flag, however you might want to add this to your answer.Incomparable
Thank you for your PR and for pointing out the issue. I have fixed in the repo and updated the answer accordingly.Pianist
@Felipe Plets, Is this supposed to work with non-relative paths? For instance, if you use a non-relative path defined via the paths property in the tsconfig? E.g. "lib/*": ["src/lib/*"]?Evalyn
Holy crap buddy, I spent ages yesterday trying to get things working. This answers everything and it works flawlessly for me. You heroDecussate
Thanks @Felipe. Can a ESM module also be imported interactively, from a REPL? #76431775Huntley
I really wanted this to work because tsx has many issues that prevent me from actually using it but the example repo and the explanations in this answer simply do not work.Clinician
--experimental-specifier-resolution=node is the solution thanks!!!!Intellectualism
I
0

I had to dowgrade my node version from 18.19.1 to 18.17.1 and it worked with setting "ts-node": { "esm": true, "experimentalSpecifierResolution": "node" }

in package.json and running with nodemon that instantiates ts-node

Ibo answered 13/12, 2023 at 8:55 Comment(0)
B
0

Use the following start script in your package.json to run your 'index.ts'

"start": "nodemon -w . --ext ts --exec ts-node ./index.ts",

Then enter 'npm start'

Buhl answered 2/2 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.