3091 words
15 minutes
TSConfig 互操作约束配置
2025-02-26 17:49:33
2025-02-27 15:33:13

← 返回 TSConfig 参考指南


允许合成默认导入 - allowSyntheticDefaultImports#

当设置为 true 时,allowSyntheticDefaultImports 允许你使用如下方式进行导入:

import React from "react";

而不是:

import * as React from "react";

这适用于模块没有显式指定默认导出的情况。

例如,如果没有将 allowSyntheticDefaultImports 设置为 true:

// @filename: utilFunctions.js const getStringLength = (str) => str.length; module.exports = { getStringLength, }; // @filename: index.ts import utils from "./utilFunctions"; // 错误:模块 "/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/utilFunctions" 没有默认导出。 const count = utils.getStringLength("Check JS");

这段代码会报错,因为没有一个可以导入的默认对象,尽管看起来应该有。为了方便使用,像 Babel 这样的转译器会在没有创建默认导出时自动创建一个。这使得模块看起来更像是这样:

// @filename: utilFunctions.js const getStringLength = (str) => str.length; const allFunctions = { getStringLength, }; module.exports = allFunctions; module.exports.default = allFunctions;

这个标志不会影响 TypeScript 生成的 JavaScript 代码,它只用于类型检查。此选项使 TypeScript 的行为与 Babel 保持一致,Babel 会生成额外的代码来使模块的默认导出使用更加符合人体工程学。

默认值: 当 esModuleInterop 启用、module 为 system 或 moduleResolution 为 bundler 时为 true;否则为 false。

相关配置: esModuleInterop

发布版本: 1.8

ES 模块互操作 - esModuleInterop#

默认情况下(当 esModuleInterop 为 false 或未设置时),TypeScript 会将 CommonJS/AMD/UMD 模块视为类似于 ES6 模块。在这种处理方式中,有两个特别存在缺陷的假设:

  1. 命名空间导入(如 import * as moment from "moment")的行为与 const moment = require("moment") 相同

  2. 默认导入(如 import moment from "moment")的行为与 const moment = require("moment").default 相同

这种不匹配导致了以下两个问题:

  1. ES6 模块规范规定命名空间导入(import * as x)只能是一个对象,但 TypeScript 将其视为与 = require("x") 相同,这就允许将导入视为函数并可调用。这不符合规范。

  2. 虽然符合 ES6 模块规范,但大多数 CommonJS/AMD/UMD 模块的库并没有像 TypeScript 的实现那样严格遵循。

启用 esModuleInterop 将修复 TypeScript 转译代码中的这两个问题。第一个改变了编译器中的行为,第二个通过两个新的辅助函数来解决,这些函数提供了一个垫片以确保生成的 JavaScript 代码的兼容性:

import * as fs from "fs"; import _ from "lodash"; fs.readFileSync("file.txt", "utf8"); _.chunk(["a", "b", "c", "d"], 2);

当 esModuleInterop 禁用时:

"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const fs = require("fs"); const lodash_1 = require("lodash"); fs.readFileSync("file.txt", "utf8"); lodash_1.default.chunk(["a", "b", "c", "d"], 2);

当 esModuleInterop 设置为 true 时:

"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs")); const lodash_1 = __importDefault(require("lodash")); fs.readFileSync("file.txt", "utf8"); lodash_1.default.chunk(["a", "b", "c", "d"], 2);

注意:命名空间导入 import * as fs from "fs" 只考虑导入对象上的自有属性(基本上是直接设置在对象上而不是通过原型链设置的属性)。如果你导入的模块使用继承属性定义其 API,你需要使用默认导入形式(import fs from "fs"),或者禁用 esModuleInterop。

注意:你可以通过启用 importHelpers 使 JavaScript 输出更简洁:

"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const fs = tslib_1.__importStar(require("fs")); const lodash_1 = tslib_1.__importDefault(require("lodash")); fs.readFileSync("file.txt", "utf8"); lodash_1.default.chunk(["a", "b", "c", "d"], 2);

启用 esModuleInterop 也会自动启用 allowSyntheticDefaultImports。

推荐:

默认值: 当 module 为 node16、nodenext 或 preserve 时为 true;否则为 false。

相关配置: allowSyntheticDefaultImports

发布版本: 2.7

强制文件名大小写一致 - forceConsistentCasingInFileNames#

TypeScript 遵循其运行所在文件系统的大小写敏感规则。如果一些开发者在大小写敏感的文件系统上工作,而其他人不是,这可能会产生问题。例如,如果一个文件试图通过指定 ./FileManager.ts 来导入 fileManager.ts,在大小写不敏感的文件系统中可以找到该文件,但在大小写敏感的文件系统中则找不到。

当启用此选项时,如果程序尝试使用与磁盘上的大小写不同的方式来包含文件,TypeScript 将发出错误。

推荐:

默认值: true

发布版本: 1.8

隔离声明 - isolatedDeclarations#

要求导出项有足够的类型注解,以便其他工具可以轻松生成声明文件。 更多信息,请参见 5.5 版本发布说明。

发布版本: 5.5

隔离模块 - isolatedModules#

虽然你可以使用 TypeScript 将 TypeScript 代码转换为 JavaScript 代码,但使用 Babel 等其他转译器来完成这项工作也很常见。然而,其他转译器一次只能处理一个文件,这意味着它们无法应用依赖于理解完整类型系统的代码转换。这个限制也适用于一些构建工具使用的 TypeScript 的 ts.transpileModule API。

这些限制可能会导致某些 TypeScript 功能(如 const enums 和命名空间)在运行时出现问题。设置 isolatedModules 标志会告诉 TypeScript,如果你编写了某些无法被单文件转译过程正确解释的代码,就会发出警告。

它不会改变你的代码的行为,也不会改变 TypeScript 的检查和生成过程的行为。

以下是一些在启用 isolatedModules 时不起作用的代码示例:

非值标识符的导出#

在 TypeScript 中,你可以导入一个类型,然后再导出它:

import { someType, someFunction } from "someModule"; someFunction(); export { someType, someFunction };

因为 someType 没有值,生成的导出不会尝试导出它(这在 JavaScript 中会是一个运行时错误):

export { someFunction };

单文件转译器不知道 someType 是否产生值,所以导出一个只引用类型的名称是错误的。

非模块文件#

如果设置了 isolatedModules,命名空间只允许在模块中使用(这意味着它有某种形式的导入/导出)。如果在非模块文件中发现命名空间,就会出现错误:

namespace Instantiated { // 错误:当启用 'isolatedModules' 时,命名空间不允许在全局脚本文件中使用。 // 如果此文件不是全局脚本文件,请将 'moduleDetection' 设置为 'force' 或添加一个空的 'export {}' 语句。 export const x = 1; }

这个限制不适用于 .d.ts 文件。

对 const enum 成员的引用#

在 TypeScript 中,当你引用一个 const enum 成员时,该引用在生成的 JavaScript 中会被替换为其实际值。将这个 TypeScript 代码:

declare const enum Numbers { Zero = 0, One = 1, } console.log(Numbers.Zero + Numbers.One);

转换为这个 JavaScript 代码:

"use strict"; console.log(0 + 1);

如果不知道这些成员的值,其他转译器就无法替换对 Numbers 的引用,如果保持原样,这将是一个运行时错误(因为运行时没有 Numbers 对象)。因此,当设置了 isolatedModules 时,引用环境 const enum 成员将是一个错误。

默认值: 当启用 verbatimModuleSyntax 时为 true;否则为 false。

发布版本: 1.5

保留符号链接 - preserveSymlinks#

这个选项反映了 Node.js 中的同名标志,它不会解析符号链接的真实路径。

这个标志的行为与 Webpack 的 resolve.symlinks 选项相反(即,将 TypeScript 的 preserveSymlinks 设置为 true 相当于将 Webpack 的 resolve.symlinks 设置为 false,反之亦然)。

启用此选项后,对模块和包的引用(例如导入和 ///指令)都是相对于符号链接文件的位置来解析的,而不是相对于符号链接解析到的路径。

发布版本: 2.5

原始模块语法 - verbatimModuleSyntax#

默认情况下,TypeScript 会执行一个叫做导入消除的操作。基本上,如果你写了这样的代码:

import { Car } from "./car"; export function drive(car: Car) { // ... }

TypeScript 会检测到你只是在使用导入的类型,并完全删除这个导入。你的输出 JavaScript 可能看起来像这样:

export function drive(car) { // ... }

大多数情况下这很好,因为如果 Car 不是从 ./car 导出的值,我们会得到一个运行时错误。

但是这确实为某些边缘情况增加了一层复杂性。例如,注意到没有类似 import "./car" 这样的语句 - 导入被完全删除了。这实际上会对模块是否有副作用产生影响。

TypeScript 生成 JavaScript 的策略还有其他几层复杂性 - 导入消除不仅仅取决于导入如何使用 - 它通常还会考虑值是如何声明的。所以像下面这样的代码是否应该保留或删除并不总是很清楚:

export { Car } from "./car";

如果 Car 是用类似类的方式声明的,那么它可以在生成的 JavaScript 文件中保留。但如果 Car 只是声明为类型别名或接口,那么 JavaScript 文件就不应该导出 Car。

虽然 TypeScript 可以根据跨文件的信息做出这些生成决策,但并不是每个编译器都能做到。

导入和导出上的 type 修饰符在某种程度上帮助解决了这些情况。我们可以通过使用 type 修饰符明确指出导入或导出是否仅用于类型分析,并可以在 JavaScript 文件中完全删除:

// 这个语句可以在 JS 输出中完全删除 import type * as car from "./car"; // 命名导入/导出 'Car' 可以在 JS 输出中删除 import { type Car } from "./car"; export { type Car } from "./car";

type 修饰符本身并不是很有用 - 默认情况下,模块消除仍会删除导入,并且没有什么强制你区分类型和普通导入导出。所以 TypeScript 有 —importsNotUsedAsValues 标志来确保你使用 type 修饰符,—preserveValueImports 来防止某些模块消除行为,以及 —isolatedModules 来确保你的 TypeScript 代码在不同编译器之间都能工作。不幸的是,理解这 3 个标志的细节很困难,并且仍然存在一些边缘情况会出现意外行为。

TypeScript 5.0 引入了一个新选项 —verbatimModuleSyntax 来简化这种情况。规则更简单 - 任何没有 type 修饰符的导入或导出都会保留。任何使用 type 修饰符的都会完全删除。

// 完全删除 import type { A } from "a"; // 重写为 'import { b } from "bcd";' import { b, type c, type d } from "bcd"; // 重写为 'import {} from "xyz";' import { type xyz } from "xyz";

使用这个新选项,你看到的就是你得到的。

不过这在模块互操作方面确实有一些影响。在这个标志下,当你的设置或文件扩展名暗示了不同的模块系统时,ECMAScript 的导入和导出不会被重写为 require 调用。相反,你会得到一个错误。如果你需要生成使用 require 和 module.exports 的代码,你必须使用 TypeScript 在 ES2015 之前的模块语法:

输入的 TypeScript输出的 JavaScript
import foo = require("foo");const foo = require("foo");
function foo() {} function bar() {} function baz() {} export = { foo, bar, baz, };

|

function foo() {} function bar() {} function baz() {} module.exports = { foo, bar, baz, };

|

虽然这是一个限制,但它确实帮助使一些问题变得更明显。例如,在 —module node16 下忘记设置 package.json 中的 type 字段是很常见的。因此,开发者会在不知不觉中开始编写 CommonJS 模块而不是 ES 模块,这会导致意外的查找规则和 JavaScript 输出。这个新标志通过使语法有意不同来确保你对正在使用的文件类型是有意识的。

因为 —verbatimModuleSyntax 提供了比 —importsNotUsedAsValues 和 —preserveValueImports 更一致的方案,这两个现有标志正在被弃用,转而支持它。

更多详情,请查看原始拉取请求和其提案问题。

发布版本: 5.0

TSConfig 互操作约束配置
https://0bipinnata0.my/posts/typescript/tsconfig/06-interop-constraints/
Author
0bipinnata0
Published at
2025-02-26 17:49:33