Search code examples
javascriptnode.jstypescriptes6-modulescommonjs

Default class instantiation results in TypeError (ESM/CJS interop)


Issue Summary

Hi,

I have a TypeScript project where I am trying to instantiate a class which was the default export of a different package. I am writing my project in ESM syntax, whereas the package it's dependent upon has CJS output. The issue I am running into is that at runtime, when the flow reaches the point of class instantiation I am getting the following error -

new TestClass({ arg1: "Hello, World!" });
^

TypeError: TestClass is not a constructor

Code

//My package.json
{
  "name": "myproject",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "type": "module",
  "dependencies": {
    "testpackage": "^1.0.0",
    "typescript": "^5.0.4"
  },
  "devDependencies": {
    "@types/node": "^20.2.5"
  }
}

//My index.ts
import TestClass from "testpackage";

new TestClass({ arg1: "Hello, World!" });
//My tsconfig.json
{
    "include": ["src"],
    "compilerOptions": {
        "outDir": "dist",
        "lib": ["es2023"],
        "target": "es2022",
        "moduleResolution": "node"
    }
}

//Dependency's package.json
{
    "name": "testpackage",
    "version": "1.0.0",
    "description": "TestPackage",
    "main": "./dist/testFile.js",
    "exports": "./dist/testFile.js",
    "scripts": {
        "build": "tsc"
    },
    "files": ["dist"],
    "devDependencies": {
        "@types/node": "^20.2.5",
        "typescript": "^5.0.4"
    }
}
//Dependency's testFile.ts
export default class TestClass {
    constructor({ arg1 }: { arg1: string }) {
        console.log(arg1);
    }
}
//Dependency's tsconfig.json
{
    "include": ["src"],
    "compilerOptions": {
        "declaration": true,
        "lib": ["es2023"],
        "target": "es6",
        "module": "CommonJS",
        "outDir": "dist"
    }
}
//Dependency's testFile.js output
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class TestClass {
    constructor({ arg1 }) {
        console.log(arg1);
    }
}
exports.default = TestClass;

Things work fine if I remove "type": "module" from my package.json. They also work fine if the class is a named export instead of a default export in the dependency's code. Is this a known incompatibility when trying to import CJS into ESM or am I doing something incorrectly here?


Note - If I set "moduleResolution": "nodenext" in my tsconfig.json then the error is generated at compile time itself -

src/index.ts:3:5 - error TS2351: This expression is not constructable.
  Type 'typeof import("<project_dir>/node_modules/testpackage/dist/testFile")' has no construct signatures.

3 new TestClass({ arg1: "Hello, World!" });
      ~~~~~~~~~


Found 1 error in src/index.ts:3

Solution

  • There are known compatibility issues between CommonJS (CJS) and ECMAScript modules (ESM). In ESM, default exports of CJS modules are wrapped in default properties instead of being exposed directly. On the other hand, named exports are unaffected and can be imported directly.

    If you specify "type": Specifying "module" in package.json makes Node.js treat the .js file as her ESM. Therefore, you must import the module using the ESM import statement. However, if the module you are trying to import is in CJS format, you will run into compatibility issues.

    There are several options to fix this.

    1. Access the class through the default property as described above.
    import Test from 'package-name';
    const TestClass = Test.default;
    
    
    1. To avoid problems caused by mixing the two module formats, convert all code to use either ESM or CJS.

    2. Load the CJS module using the Node.js createRequire function.

    import { createRequire } from 'module';
    const require = createRequire(import.meta.url);
    const TestClass = require('package-name');