javascript - Zadano instanciranje klase rezultira TypeError-om (ESM/CJS interoperabilnost)


Sažetak problema

Bok,

Imam TypeScript projekt u kojem pokušavam instancirati klasu koja je bila zadani izvoz iz drugog paketa. Moj projekt pišem u ESM sintaksi, dok paket na kojem ovisi ima CJS izlaz. Problem na koji nailazim je da u runtimeu, kada tok izvršavanja dosegne točku instanciranja klase, dobivam sljedeću grešku -

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

TypeError: TestClass is not a constructor

Kod

//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;

Stvari rade u redu ako uklonim "type": "module" iz mog package.json. Također rade u redu ako je klasa imenovani export umjesto default export u kodu zavisnosti. Je li ovo poznata nekompatibilnost prilikom pokušaja uvoza CJS u ESM ili radim nešto pogrešno ovdje?


Napomena - Ako postavim "moduleResolution": "nodenext" u svom tsconfig.json, tada se pogreška generira već u vrijeme kompajliranja -

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

Odgovor #1

Poznati su problemi kompatibilnosti između CommonJS (CJS) i ECMAScript modula (ESM). U ESM-u, zadani eksporti CJS modula umotani su u svojstva 'default' umjesto da se izlažu izravno. S druge strane, imenovani eksporti nisu pogođeni i mogu se uvoziti izravno.

Ako navedete "type": Navođenje "module" u datoteci package.json čini da Node.js tretira .js datoteku kao ESM. Stoga, morate uvesti modul koristeći ESM naredbu za uvoz. Međutim, ako je modul koji pokušavate uvesti u CJS formatu, naići ćete na probleme s kompatibilnošću.

Postoji nekoliko opcija za rješavanje ovoga.

  1. Pristupite klasi putem zadane svojstva kao što je opisano gore.
import Test from 'package-name';
const TestClass = Test.default;

  1. Kako biste izbjegli probleme uzrokovane miješanjem dvaju formata modula, pretvorite sav kod da koristi ili ESM ili CJS.

  2. Učitajte CJS modul koristeći Node.js funkciju createRequire.

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