在 Typescript 编译(ES6 模块)期间在相对导入语句上附加 .js 扩展名

Appending .js extension on relative import statements during Typescript compilation (ES6 modules)

这似乎是一个微不足道的问题,但是需要用什么settings/configurations来解决这个问题并不是很明显。

Hello World程序目录结构和源代码如下:

目录结构:

| -- HelloWorldProgram
     | -- HelloWorld.ts
     | -- index.ts
     | -- package.json
     | -- tsconfig.json

index.ts:

import {HelloWorld} from "./HelloWorld";

let world = new HelloWorld();

HelloWorld.ts:

export class HelloWorld {
    constructor(){
        console.log("Hello World!");
    }
}

package.json:

{
  "type": "module",
  "scripts": {
    "start": "tsc && node index.js"
  }
}

现在,执行命令 tsc && node index.js 会导致以下错误:

internal/modules/run_main.js:54
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'HelloWorld' imported from HelloWorld\index.js
Did you mean to import ../HelloWorld.js?
    at finalizeResolution (internal/modules/esm/resolve.js:284:11)
    at moduleResolve (internal/modules/esm/resolve.js:662:10)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:752:11)
    at Loader.resolve (internal/modules/esm/loader.js:97:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:242:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:50:40)
    at link (internal/modules/esm/module_job.js:49:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}

很明显,问题似乎源于 index.ts Typescript 文件的导入语句 (import {HelloWorld} from "./HelloWorld";) 中没有 .js 扩展名。 Typescript 在编译期间没有抛出任何错误。但是,在运行时 Node (v14.4.0) 需要 .js 扩展。

希望上下文清楚。

现在,如何更改编译器输出设置(tsconfig.json 或任何标志),以便在 Typescript 到 Javascript 编译在 index.js 文件中?

注意: It is possible to directly use the .js extension while importing inside typescript file. However, it doesn't help much while working with hundreds of old typescript modules, because then we have to go back and manually add .js extension. Rather than that for us better solution is to batch rename and remove all the .js extension from all the generated .js filenames at last.

我通常也只在 typescript 文件的导入语句中使用 .js 扩展名,而且它有效。

在导入路径中不使用文件扩展名是 nodejs 独有的。由于您没有使用 commonjs 而是模块,因此您没有使用 nodejs。因此,您必须在导入路径中使用 .is 扩展名。

TypeScript 不可能 知道您将使用什么 URI 来提供文件,因此它只是 必须 信任模块路径你给的是对的。在这种情况下,你给了它一个不存在的 URI 路径,但 TypeScript 不知道,所以它无能为力。

如果您使用以 .js 结尾的 URI 为模块提供服务,那么您的模块路径需要以 .js 结尾。如果您的模块路径不以 .js 结尾,那么您需要在不以 .js.

结尾的 URI 中提供它

请注意 the W3C strongly advises against using file extensions in URIs,因为它会让您的系统更难发展,因此提倡依赖内容协商。

重写路径会破坏 TypeScript 的几个基本设计原则。一个设计原则是 TypeScript 是 ECMAScript 的适当超集,并且每个有效的 ECMAScript 程序和模块都是语义上等效的 TypeScript 程序和模块。重写路径会破坏该原则,因为一段 ECMAScript 的行为会有所不同,具体取决于它是作为 ECMAScript 还是作为 TypeScript 执行的。想象一下,您有以下代码:

./你好

export default "ECMAScript";

./hello.js

export default "TypeScript";

./main

import Hello from "./hello";

console.log(Hello);

如果 TypeScript 按照您的建议执行,这将打印 两种不同的东西,具体取决于您是将其作为 ECMAScript 还是作为 TypeScript 执行,但 TypeScript 设计原则表明 TypeScript 永远不会改变 ECMAScript 的含义。当我将一段 ECMAScript 作为 TypeScript 执行时,它的行为应该 完全 就像我将它作为 ECMAScript 执行时一样。

对于正在寻找此问题解决方案的开发人员,我们遇到的可能的解决方法如下:

  1. 对于新文件,可以在编辑时在Typescript文件的导入语句中简单地添加".js"扩展名。示例:import {HelloWorld} from "./HelloWorld.js";

  2. 如果使用旧项目,而不是遍历每个文件并更新导入语句,我们发现简单地批量重命名并从生成的文件中删除 ".js" 扩展名更容易Javascript 通过一个简单的自动化脚本。但是请注意,这可能需要对服务器端代码进行微小更改,以便为客户端提供这些具有正确 MIME 类型的无扩展名 ".js" 文件。如果您想避免这种情况,您可能希望使用正则表达式以递归方式批量查找和替换导入语句以添加 .js 扩展名。

旁注:

关于 TS 团队未能解决这个问题,似乎有一种倾向,试图断章取义地夸大这个问题,而不是真正的问题,并将其附加到一些设计原则上来捍卫。

然而,实际上这只不过是 编译器如何不对称地处理扩展 的问题。 Typescript 编译器允许不带扩展名的 import 语句。然后它继续在翻译文件时向相应的输出文件名添加“.js”扩展名,但对于引用此文件的相应导入语句,它忽略了它在翻译期间添加了“.js”扩展名的事实。脱离上下文的 URI 重写原则如何保护这种不对称性?

编译时Typescript文件和生成的Javascript输出文件之间存在固定的一一对应关系。如果引用的导入不存在,编译器将抛出错误。这些文件甚至无法编译!因此,在上下文之外或不可编译的示例中提及其他冲突 URI 的可能性会使此类声明无效。

如果编译器简单地生成无扩展名的输出文件,它也可以解决问题。但是,这是否也会以某种方式违反有关 URI 重写的设计原则?当然,在那种情况下,可能存在其他设计原则来捍卫这一立场!但是这样的固执岂不是更进一步印证了TS团队在这个问题上的固执还是无知?

正如许多人指出的那样。 Typescript 不会也永远不会向 import 语句添加文件扩展名的原因是他们的前提是转译纯 javascript 代码应该输出相同的 javascript 代码。

我认为 having a flag to make typescript enforce file extensions in import statements would be the best they could do. Then linters like eslint 可以根据该规则提供自动修复程序

您还可以添加 nodejs CLI 标志以启用 node module resolution:

  • 用于导入 json --experimental-json-modules
  • 用于不带扩展名的导入 --experimental-specifier-resolution=node

我知道 --experimental-specifier-resolution=node 有一个错误(或没有)然后你不能 运行 没有扩展的 bin 脚本(例如在 package.json bin "tsc" 中不起作用,但是"tsc":"tsc.js" 将起作用)。 许多软件包都有没有任何扩展名的 bin 脚本,因此添加 NODE_OPTIONS="--experimental-specifier-resolution=node" env 变量

会遇到一些麻烦

你可以使用和我一样的解决方案

文件:tsconfig.json

"compilerOptions": {
    "module": "commonjs",   ==> not required extension when import   
    "target": "ES6",
},

因为使用了 commonjs,你必须删除 package.json

中的 "type": "module"

完成:D

如果您在使用 TypeScript 和 ESM 时遇到问题,这个小型库实际上可以完美运行。

npm install @digitak/tsc-esm --save-dev

在您的脚本中用 tsc-esm 替换 tsc 调用:

{
   "scripts": {
      "build": "tsc-esm"
   }
}

终于可以运行:

npm run build

为了修复此错误,您可以按如下方式更改 tsconfig.json 文件:

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "resolveJsonModule": true
  }
}

这是NestJS使用的配置。