为什么打字稿允许我导入它在运行时无法使用的依赖项?

Dan*_*lan 13 javascript commonjs node.js typescript es6-modules

您可以在这里查看我的示例项目:https://github.com/DanKaplanSES/typescript-stub-examples/tree/JavaScript-import-invalid

\n

我创建了这个名为 main.ts 的文件:

\n
import uuid from "uuid";\n\nconsole.log(uuid.v4());\n\n
Run Code Online (Sandbox Code Playgroud)\n

虽然打字稿适合这种导入,但当我尝试node main.js,它会给出以下错误:

\n
console.log(uuid_1["default"].v4());\n                              ^\n\nTypeError: Cannot read property \'v4\' of undefined\n    at Object.<anonymous> (C:\\root\\lib\\main.js:5:31)\n\xe2\x86\x90[90m    at Module._compile (internal/modules/cjs/loader.js:1063:30)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Module.load (internal/modules/cjs/loader.js:928:32)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Function.Module._load (internal/modules/cjs/loader.js:769:14)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at internal/main/run_main_module.js:17:47\xe2\x86\x90[39m\n
Run Code Online (Sandbox Code Playgroud)\n

如果我将文件更改为此,它会正常执行:

\n
console.log(uuid_1["default"].v4());\n                              ^\n\nTypeError: Cannot read property \'v4\' of undefined\n    at Object.<anonymous> (C:\\root\\lib\\main.js:5:31)\n\xe2\x86\x90[90m    at Module._compile (internal/modules/cjs/loader.js:1063:30)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Module.load (internal/modules/cjs/loader.js:928:32)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Function.Module._load (internal/modules/cjs/loader.js:769:14)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)\xe2\x86\x90[39m\n\xe2\x86\x90[90m    at internal/main/run_main_module.js:17:47\xe2\x86\x90[39m\n
Run Code Online (Sandbox Code Playgroud)\n

如果第一个版本无效,为什么打字稿不通知我?

\n

我有一个多文件 tsconfig 设置。检查 github 项目以获取更多详细信息,但以下是可能相关的共享编译器选项:

\n
import * as uuid from "uuid";\n\nconsole.log(uuid.v4());\n
Run Code Online (Sandbox Code Playgroud)\n

main.js 的外观如下:

\n

不起作用

\n
{\n    "compilerOptions": {\n        "rootDir": ".",\n        "esModuleInterop": true,\n        "module": "CommonJS",\n        "moduleResolution": "node",\n        "composite": true,\n        "importHelpers": true,\n    },\n}\n
Run Code Online (Sandbox Code Playgroud)\n

作品

\n
"use strict";\nexports.__esModule = true;\nvar tslib_1 = require("tslib");\nvar uuid_1 = tslib_1.__importDefault(require("uuid"));\nconsole.log(uuid_1["default"].v4());\n
Run Code Online (Sandbox Code Playgroud)\n

Dan*_*lan 11

I kind of feel guilty answering my own bounty question, so I'm going to mark it as community. The reason I'm writing my own is because I feel like the other answer really buries the lede. I had to perform hours and hours of my own research after reading it to be able to write this. That being the case, I think my answer will be more helpful to people that start in my boat, where I didn't know what I didn't know. I also think there's an additional solution to the problem, though it would be more invasive.

My original question was, "If the first version is invalid, why doesn't typescript inform me?" Here is the other answer's explanation:

Because you have enabled esModuleInterop which also enables allowSyntheticDefaultImports. The CommonJS bundle is actually incompatible with that option but TypeScript doesn't know.

This is absolutely true, but when it comes to understanding what's going on, it's the tip of the iceberg:

If you look at the reference documentation, it recommends you set esModuleInterop to true. Why would it make that recommendation if it reduces type safety? Well, that is not why it recommends you set it to true. In fact, this setting does not reduce type safety -- it increases it by fixing some legacy typescript bugs, specifically two that deal with how typescript handles requires. You can read the documentation for more details on that, but in my opinion, if you are using node libraries, I think it's a good idea to set esModuleInterop to true.

But! esModuleInterop has a side effect. At the very bottom of its documentation, it says:

Enabling esModuleInterop will also enable allowSyntheticDefaultImports.

Err... kinda. IMO, this documentation is incorrect. What it should really say is, "Enabling esModuleInterop will default allowSyntheticDefaultImports to true." If you look at the allowSyntheticDefaultImports documentation, it says this on the right-hand side:

在此输入图像描述

Hey, notice how in that upper right-hand corner it doesn't say this setting is recommended? That's probably because this setting reduces type safety: it allows you to type import React from "react"; instead of import * as React from "react"; when the module does not explicitly specify a default export.

Normally (i.e. with allowSyntheticDefaultImports set to false), this would be an error... because it is: you shouldn't be able to default import a module unless it has a default export. Setting this to true makes the compiler say, "Nah, this is fine."

但是,当您将 allowedSyntheticDefaultImports 设置为 true 时,“此标志不会影响 TypeScript 发出的 JavaScript”。这意味着,这个标志让您可以假装该库是在编译时以一种方式编写的,即使事实并非如此。在运行时,这会出错。为什么这个设定会存在?我不知道,但这可能与历史原因有关:

此选项使 TypeScript 的行为与 Babel 保持一致,其中发出额外的代码以使使用模块的默认导出更加符合人体工程学。

似乎曾经(/现在?)有一个时间点,每个人都被认为在使用 Babel。我没有这样做,所以“人体工程学”的好处变成了运行时错误。

作为一种更简洁的方法,您应该使用 import { v4 } from 'uuid'; 导入 uuid;

确实如此,但我认为将allowSyntheticDefaultImports 显式设置为 false 也是一个好主意。它为您提供了更多的类型安全性。不仅如此,它还会产生import uuid from "uuid";编译时错误(应该是这样)。

还有一件事我不明白:

将allowSyntheticDefaultImports 设置为 false 也会导致导入import os from "os";import _ from "lodash";编译时错误。但当allowSyntheticDefaultImports为true时,这些总是运行良好。一定有一些我遗漏的部分可以解释为什么这些有效但uuid无效。

我在我的 node_modules 中找不到 的来源os,但我可以查看lodash,它的index.js作用是:

module.exports = require('./lodash');
Run Code Online (Sandbox Code Playgroud)

在该必需文件中,它在底部这样说:

...
/*--------------------------------------------------------------------------*/

  // Export lodash.
  var _ = runInContext();

  // Some AMD build optimizers, like r.js, check for condition patterns like:
  if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
    // Expose Lodash on the global object to prevent errors when Lodash is
    // loaded by a script tag in the presence of an AMD loader.
    // See http://requirejs.org/docs/errors.html#mismatch for more details.
    // Use `_.noConflict` to remove Lodash from the global object.
    root._ = _;

    // Define as an anonymous module so, through path mapping, it can be
    // referenced as the "underscore" module.
    define(function() {
      return _;
    });
  }
  // Check for `exports` after `define` in case a build optimizer adds it.
  else if (freeModule) {
    // Export for Node.js.
    (freeModule.exports = _)._ = _;
    // Export for CommonJS support.
    freeExports._ = _;
  }
  else {
    // Export to the global object.
    root._ = _;
  }

Run Code Online (Sandbox Code Playgroud)

我不太明白这一切是做什么的,但我认为这是定义一个_在运行时命名的全局变量?我想这意味着,从打字稿的角度来看,这是巧合的。类型声明文件没有默认值,这通常会导致运行时错误,但几乎巧合的是,这一切最终都成功了,因为 lodash javascript 定义了一个全局_? 耸耸肩也许这也是一种使用的模式os,但我已经花了足够的时间研究这个,所以我将把它留到另一天/问题。


Twi*_*her 7

您的问题与 TypeScript/ECMAScript 模块和 CommonJS 之间的互操作性有关。

当谈到ECMAScript 模块CommonJS模块之间的差异时:

  • CommonJS模块的导入方式类似于const library = require('library')允许检索exports该库的完整对象。CommonJS中没有默认导入的概念
  • ECMAScript 模块export对于每个导出的项目都有明确的子句。它们还具有默认的导入语法,允许default在局部变量中检索导出。

为了实现CommonJS模块和 TypeScript 的默认导入语法之间的互操作性,CommonJS模块可以有一个default属性。

该属性甚至可以在启用default时由 TypeScript 自动添加(这也会启用)。此选项在转译时添加此辅助函数:esModuleInteropallowSyntheticDefaultImports

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Run Code Online (Sandbox Code Playgroud)

基本上这个函数的作用是:如果导入的模块的标志__esModule设置为 true,则按原样导出它,因为该模块旨在用作ECMAScript 模块import { feature } from 'library'。否则,将其导出到带有属性的包装对象内default,该属性启用import localName from 'library'语法。

uuid正在构建的包包含@babel/plugin-transform-modules-commonjs__esModule标志并阻止您使用默认的导入语法。其他软件包(如lodash不包含此标志)允许 TypeScript 添加该default属性。

总之,TypeScript 提供了与旧版CommonJS模块互操作的选项,但这些选项不适用于“ECMAScript 感知” CommonJS模块。TypeScript 无法在转译时发出警告或错误,因为CommonJS模块接口除了对象之外没有任何表示形式exports,而对象仅在运行时才知道。