简体   繁体   中英

Unable to import ESM module in Nestjs

I am having a problem with importing ESM modules in my project based on Nest.js. As far as I understand, this problem is relevant not just to Nest.js but typescript as well.

I have tried various things and combinations of Node.js & typescript versions, adding "type":"module" to package.json & changes in the settings of my tsconfig.json file, so it has the following view, which is far from default values:

{
  "compilerOptions": {
    "lib": ["ES2020"],
    "esModuleInterop": true,
    "module": "NodeNext",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "Node",
    "target": "esnext",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
  }
}

My full environment is:

  • Node.js (19.2.1 LTS) managed via nvm
  • Typescript (4.9.4, but I also tried 4.3.5)
  • @nestjs/common: 9.2.1
  • @nestjs/core: 9.2.1
  • ts-loader: "9.4.2",
  • ts-node: "10.9.1",
  • tsconfig-paths: "4.1.0",

But it still gives me an error when I am trying to import any ESM module in any of my services. For example:

import random from `random`;

export class AppService implements OnApplicationBootstrap {
  async test() {
     const r = random.int(1, 5);
     console.log(r);
  }
}

Does anyone have a clue how to fix it?

This Problem seems to occur more frequently since more packages are switching over to be distributed as ES module.

Summary

  • ES Modules can import CommonJS modules
  • CommonJS modules cannot import ES Modules synchronously
  • NestJS does currently not support to compile to ESM - see this PR

There are two approaches for this problem.

Import package asynchronously using the dynamic import() function

The instruction to use import() for ES modules in CommonJS can be found everywhere. But when using typescript the additional hint is missing how to prevent the compiler to transform the import() call to a require() . I found two options for this:

  • set moduleResolution to nodenext or node16 in your tsconfig.json (Variant 1)
  • use the eval workaround - this is based on the section "Solution 2: Workaround using eval" from answer https://stackoverflow.com/a/70546326/13839825 (Variant 2 & 3)

Variant 1: await import() whenever needed

This solution is also found frequently on the official NestJS Discord

  • add "moduleResolution": "nodenext" or "moduleResolution": "node16" to your tsconfig.json
  • use an await import() call when needed
  • simple and straight forward
  • package is not listed on top with other imports
  • you cannot import/use types from the imported ES module (at least I found no possibility so far)
  • only works in async context
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    const random = (await import('random')).default;
    return 'Hello World! ' + random.int(1, 10);
  }
}

Variant 2: Helper Function

Note : Importing a type from a ES module will not result in an require call, since it's only used by the typescript compiler and not the runtime environment.

  • a bit messy because package is always hidden behind an extra function call
  • only works in async context
  • you can import/use types
import { Injectable } from '@nestjs/common';
import { type Random } from 'random';

async function getRandom(): Promise<Random> {
  const module = await (eval(`import('random')`) as Promise<any>);
  return module.default;
}

@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    return 'Hello World! ' + (await getRandom()).int(1, 10);
  }
}

Variant 3: Import and save to local variable

  • no extra function call needed
  • imported module can be undefined at runtime (unlikely but still bad practice)
  • you can import/use types
import { Injectable } from '@nestjs/common';
import { type Random } from 'random';

let random: Random;
eval(`import('random')`).then((module) => {
  random = module.default;
});


@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    return 'Hello World! ' + random.int(1, 10);
  }
}

Convert your NestJS project to ES modules (not recommended)

Although not supported, it seems possible to setup NestJS to compile to ESM. This Guide has good general instructions to do this in a typescript project.

I tested it with NestJS and found these steps sufficient:

  • add "type": "module" to your package.json
  • change module to NodeNext in your compilerOptions in tsconfig.json
  • add the .js extension to all of your relative imports

Now the import should work as expected

import { Injectable } from '@nestjs/common';
import random from 'random';

@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    return 'Hello World! ' + random.int(1, 10);
  }
}

What did not work so far were the unit tests with jest. I got other import errors there and I bet there are more problems down the road. I would avoid this approach and wait until NestJS officially supports ES modules.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM