網站搜尋

javascript - 為什麼 TypeScript 允許我導入在運行時無法使用的依賴項?


你可以在這裡看到我的範例專案:https://github.com/DanKaplanSES/typescript-stub-examples/tree/JavaScript-import-invalid

我已經創建了這個名為 main.ts 的檔案:

import uuid from "uuid";

console.log(uuid.v4());

雖然 TypeScript 對這個匯入沒問題,但當我嘗試 node main.js 時,它會出現這個錯誤:

console.log(uuid_1["default"].v4());
                              ^

TypeError: Cannot read property 'v4' of undefined
    at Object.<anonymous> (C:\root\lib\main.js:5:31)
←[90m    at Module._compile (internal/modules/cjs/loader.js:1063:30)←[39m
←[90m    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)←[39m
←[90m    at Module.load (internal/modules/cjs/loader.js:928:32)←[39m
←[90m    at Function.Module._load (internal/modules/cjs/loader.js:769:14)←[39m
←[90m    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)←[39m
←[90m    at internal/main/run_main_module.js:17:47←[39m

如果我將檔案改成這樣,它就能正常執行:

import * as uuid from "uuid";

console.log(uuid.v4());

如果第一個版本無效,為什麼 TypeScript 不通知我?

我有一個多文件的 tsconfig 設置。查看 GitHub 項目以獲取更多細節,但這裡是可能相關的共享編譯器選項:

{
    "compilerOptions": {
        "rootDir": ".",
        "esModuleInterop": true,
        "module": "CommonJS",
        "moduleResolution": "node",
        "composite": true,
        "importHelpers": true,
    },
}

這是 main.js 的樣子:

不起作用

"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
var uuid_1 = tslib_1.__importDefault(require("uuid"));
console.log(uuid_1["default"].v4());

作品

"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
var uuid = tslib_1.__importStar(require("uuid"));
console.log(uuid.v4());

答案#1

我有點為自己回答自己的懸賞問題感到內疚,所以我打算將它標記為社群答案。我自己寫的原因是因為我覺得其他答案真的掩蓋了重點。我在讀過它之後,花了好幾個小時自己的研究,才寫出這個答案。既然如此,我覺得我的答案對於從我這個起點開始的人會更有幫助,當時我還不知道自己不知道的事情。我也認為這個問題有額外的解決方案,雖然它會比較具侵入性。

我最初的問題是,『如果第一個版本無效,為什麼 TypeScript 不告訴我?』這是另一個答案的解釋:

因為你已啟用 esModuleInterop,這也會啟用 allowSyntheticDefaultImports。CommonJS 捆綁其實與這個選項不相容,但 TypeScript 並不知道。

這絕對是真的,但當談到理解正在發生的事情時,這只是冰山一角:

如果你查看參考文檔,它建議你將esModuleInterop設置為 true。為什麼它會提出這樣的建議,明明這樣做會降低類型安全呢?嗯,這並不是它建議你設置為 true 的原因。事實上,這個設置並不降低類型安全——它通過修正一些舊的 typescript 錯誤來提高類型安全,特別是兩個與 typescript 處理 requires 的方式有關的錯誤。你可以查看文檔以獲取更多細節,但依我看,如果你在使用 node 庫,我認為將 esModuleInterop 設置為 true 是個好主意。

但是!esModuleInterop 有一個副作用。在其文件的最底部,它寫道:

啟用 esModuleInterop 也會啟用 allowSyntheticDefaultImports。

嗯……有點。依我看,這份文件是錯的。它真正應該說的是,"啟用 esModuleInterop 將 默認 將 allowSyntheticDefaultImports 設為 true。" 如果你看 allowSyntheticDefaultImports 的文件,它在右側是這麼說的:

嘿,注意到右上角那裡沒有說這個設定是推薦的嗎?那可能是因為這個設定會降低類型安全性:它允許你在模組沒有明確指定預設導出時,使用 import React from "react"; 而不是 import * as React from "react";。

通常(也就是將 allowSyntheticDefaultImports 設為 false 時),這會是個錯誤……因為確實如此:你不應該能夠默認導入一個模組,除非它有默認導出。將這個設為 true 會讓編譯器說:「不,沒問題。」

但是,當你將 allowSyntheticDefaultImports 設為 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');

在那個必要的檔案中,底部寫著這個:

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

  // 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._ = _;
  }

我其實不太明白這整段程式碼在做什麼,但我覺得這是在運行時定義一個名為 _ 的全域變數?我猜這意味著,從 TypeScript 的角度來看,這碰巧是可行的。型別宣告檔沒有預設值,通常這會導致運行時錯誤,但幾乎碰巧的是,最終都能正常運作,因為 lodash 的 JavaScript 定義了一個全域 _?聳聳肩 也許這也是 os 採用的模式,但我已經花了足夠多的時間研究這個問題,所以我會留待以後的另一個日子/問題再處理。

答案#2

您的問題與 TypeScript/ECMAScript 模組與 CommonJS 之間的互操作性有關。

談到 ECMAScript 模組與 CommonJS 模組之間的差異時:

  • CommonJS 模組應該被這樣導入:const library=require('library'),這樣可以取得該模組的完整 exports 物件。在 CommonJS 中不存在預設導入的概念
  • ECMAScript 模組對每個導出的項目都有明確的 export 子句。它們還具有一個預設匯入語法,可用來在本地變數中取得 default 導出。

為了實現 CommonJS 模組與 TypeScript 的預設匯入語法之間的互通性,CommonJS 模組可以有一個 default 屬性。

當啟用 esModuleInterop(這也會啟用 allowSyntheticDefaultImports)時,TypeScript 甚至可以自動添加該 default 屬性。此選項會在轉譯時添加這個輔助函數:

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

基本上這個函數的作用是:如果導入的模組有設置 __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 物件外沒有其他表示,而這個物件僅在執行時才知道。