23

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
SamuraiJack
  • 5,131
  • 15
  • 89
  • 195
  • Simply remove the *.js* from import. If you want the extension see [this](https://stackoverflow.com/questions/56199426/typescript-import-with-extension). You can change from Node JS to [Deno](https://deno.land/manual/getting_started/typescript) 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. – Carlo Corradini Sep 04 '20 at 14:17
  • @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. – SamuraiJack Sep 04 '20 at 14:20
  • In tsconfig.json (create one if not present) in *compilerOptions* add the following line: **"module": "commonjs"**. Typescript info page [here](https://www.typescriptlang.org/docs/handbook/modules.html). Tsconfig info page [here](https://www.typescriptlang.org/tsconfig#module). – Carlo Corradini Sep 04 '20 at 14:27
  • That's how it already is. I have added tsconfig in my post. – SamuraiJack Sep 04 '20 at 14:32
  • I created a simple example [here](https://stackblitz.com/edit/typescript-alpz7n?devtoolsheight=33&file=index.ts). You are trying to call the function log without using the class Util. More documentation [here](https://www.typescriptlang.org/docs/handbook/classes.html). – Carlo Corradini Sep 04 '20 at 14:42
  • does it have to be static? – SamuraiJack Sep 04 '20 at 15:00
  • From [*Static Properties*](https://www.typescriptlang.org/docs/handbook/classes.html#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. – Carlo Corradini Sep 04 '20 at 15:02
  • I suggest you to read the book [**Programming TypeScript: Making Your JavaScript Applications Scale**](https://www.oreilly.com/library/view/programming-typescript/9781492037644/). It's a solid reference for the Typescript world. – Carlo Corradini Sep 04 '20 at 15:06
  • @SamuraiJack 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. – Felipe Plets Dec 06 '20 at 00:59
  • @SamuraiJack does my answer helped? – Felipe Plets Feb 03 '21 at 14:21

2 Answers2

68

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.

Felipe Plets
  • 7,230
  • 3
  • 36
  • 60
  • 1
    working for me, with ts-node 10.7.0 in a docker environment...without ```--experimental-specifier-resolution=node``` ts-node wanted a ```.js``` file extension – J.E.C. Mar 10 '22 at 14:43
  • "--experimental-specifier-resolution=node" helped me - thanks! Eh ESM is such a tragedy for node.js development. – RushPL Mar 13 '22 at 06:34
  • 4
    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`. – kshetline May 06 '22 at 19:16
  • @kshetline can you share a repo on GitHub reproducing the issue? – Felipe Plets May 06 '22 at 19:25
  • @Felipe Plets, sure: https://github.com/kshetline/geo-db-updater/tree/development-stackoverflow-example. – kshetline May 06 '22 at 19:51
  • @kshetline the path was missing "src/" before the file namw. I've opened a PR shoing you how to fix it: https://github.com/kshetline/geo-db-updater/pull/1 – Felipe Plets May 06 '22 at 22:48
  • @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. – kshetline May 06 '22 at 23:36
  • 1
    for use with `ts-node`, you can now run `ts-node --esm index.ts` or `ts-node-esm index.ts`: https://github.com/TypeStrong/ts-node#esm – Zaytri May 30 '22 at 01:04
  • 1
    Thank you @Zaytri for the information. I have updated my answer and provided a repository with the latest configuration. – Felipe Plets May 30 '22 at 05:57
  • 1
    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. – einSelbst Sep 12 '22 at 00:43
  • Thank you for your PR and for pointing out the issue. I have fixed in the repo and updated the answer accordingly. – Felipe Plets Sep 15 '22 at 01:08
  • @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/*"]? – octavemirbeau Oct 07 '22 at 19:44
  • 1
    Holy crap buddy, I spent ages yesterday trying to get things working. This answers _everything_ and it works flawlessly for me. You hero – Alex McCabe Nov 10 '22 at 09:32
  • Thanks @Felipe. Can a ESM module also be `import`ed interactively, from a REPL? https://stackoverflow.com/questions/76431275/typescript-how-to-interactively-import-an-esm-from-repl – VanillaDonuts Jun 08 '23 at 11:35
22

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
derpedy-doo
  • 1,097
  • 1
  • 18
  • 22