typescripttsconfigts-nodets-node-dev

How to use normal imports and top-level await at the same time?


I want to use imports (import x from y) and top-level awaits at the same time with ts-node. However if I change my tsconfig.compilerOptions.module to es2017 or higher as required by top-level awaits I get:

SyntaxError: Cannot use import statement outside a module

The fix for this is according to countless GH issues and SO questions to set tsconfig.compilerOptions.module to commonjs which in turn results in:

Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', or 'nodenext', and the 'target' option is set to 'es2017' or higher

How can I have both? There has to be a way...

tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "module": "esnext",
    "target": "es2017",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "sourceMap": true,
    "outDir": "dist",
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*.ts"]
}

package.json:

{
  "name": "x",
  "version": "0.0.1",
  "main": "main.js",
  "type": "module",
  ...
}

I am using Node LTS (v16.14.2) and TypeScript 4.6.3.


Solution

  • I believe this was primarily an issue for you because you used "moduleResolution: "node" in your tsconfig.json. Nowadays you're much better off setting "module": "nodenext" and "moduleResolution: "nodenext".

    This answer has nothing to do with ts-node but only involves Node.js and TypeScript. You're better off using tsx which requires no configuration instead of ts-node.

    Given the following project:

    .
    ├── src/
    │   ├── y.ts
    │   └── main.ts
    ├── package.json
    └── tsconfig.json
    

    package.json

    "type": "module"
    

    tsconfig.json

    {
      "compilerOptions": {
         "target": "ESNext",
         "module": "NodeNext",
         "moduleResolution": "Nodenext",
         "outDir": "dist"
      },
      "include": ["src"]
    }
    

    src/y.ts

    const doSomethingAsync = () => Promise.resolve('something async')
    
    export { doSomethingAsync }
    export default 'y'
    

    src/main.ts

    import { doSomethingAsync } from './y.js'
    import x from './y.js'
    
    console.log(await doSomethingAsync())
    console.log(x)
    

    Now run ./node_modules/.bin/tsc to generate the build in dist/.

    Now run node dist/main.js:

    something async
    y
    

    The following versions of Node.js and TypeScript were used:

    If you need to use ts-node I can dig in further, but you didn't really explain why you needed that tool. Also, I would consider tsx over ts-node if you want to execute .ts files directly for whatever reason.

    Using tsx@4.9.3 you can get the same output by running:

    ./node_modules/.bin/tsx src/main.ts