Skip to content

Modules: TypeScript

参考资料

来源: Type stripping

作者: Node.js

译者: SenaoXi

额外资料:

致读者

本译文在忠于原文的基础上,融入了译者基于自身领域知识所添加的解释性内容和本地化表达。这些精心添加的内容旨在增进读者对原文核心信息的理解。如对内容有任何疑问,欢迎您参与评论区讨论,或查阅原文以作参考。

启用

node.js 中启用运行时 typescript 支持有两种方式:

  1. 如果需要完整支持所有 typescript 的语法特性,包括使用任意版本的 typescript,可以使用第三方包。
  2. 如果只需要轻量级支持,可以使用 node.js 内置的类型擦除功能。

完整 typescript 支持

可以通过第三方包来获得完整的 typescript 支持(包括对 tsconfig.json 的配置支持)。这里以 tsx 为例进行说明,当然市面上还有许多类似的库可供选择。

首先,需要使用项目中的包管理器将其安装为开发依赖。比如使用 npm

bash
npm install --save-dev tsx

然后,可以通过以下方式运行 typescript 代码:

bash
npx tsx your-file.ts

或者,通过 node 命令运行:

bash
node --import=tsx your-file.ts

类型擦除

进程

添加于: v22.6.0

稳定性: 1.1: Active development

默认启用: v23.6.0(2025-01-07)。v23.6.0 版本中 默认启用--experimental-strip-types 标志,node.js 将能够执行 typescript 文件而无需额外配置。在 type-stripping 中记录的支持语法存在一些限制。这个功能是实验性的,可能会发生变化。

相关资料:

启用 --experimental-strip-types 标志,node.js 可以直接运行 typescript 文件。默认情况下,node.js 只会执行那些不包含需要转换的 typescript 特性(如枚举或命名空间)的文件。

启用这个特性后,node.js 会将 内联类型注释 替换为空白字符,且 不会执行类型检查。如果需要启用复杂类型转换(如枚举或命名空间),可以使用 --experimental-transform-types 标志,这个标志在 node.jsv22.7.0 版本中实验性支持。

需要注意的是,依赖于 tsconfig.json 设置的 typescript 特性

  • 路径映射功能tsconfig 配置文件中的 paths 字段不会被转换,因此会产生错误。最接近的可用特性是子路径导入,限制是路径需要以 # 开头。
  • 将新版本的 javascript 语法转换为旧版本的功能。

是特意不被支持的。如果需要完整的 typescript 支持,请参考上文的 完整 typescript 支持 说明。

类型移除特性的 设计理念是保持轻量级,通过以下两个核心策略实现这一目标:

  • 有意不支持需要额外生成 javascript 代码的类型语法。
  • 将内联类型语法替换为空白字符。

由于上述两个核心策略,对于源码映射的影响基本是微乎其微,node.js 不需要再额外维护源映射。

当启用 --experimental-transform-types 时,源映射默认开启。

虽然类型擦除功能与大多数版本的 typescript 都兼容,但建议使用 5.7 或更新版本,并使用以下 tsconfig.json 配置:

json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true,
    "verbatimModuleSyntax": true
  }
}

确定模块系统

node.jstypescript 文件中同时支持 commonjsesm 语法。需要注意的是,node.js 不会在这两种模块系统之间进行转换。如果代码作为 esm 模块运行,就必须使用 importexport 语法;如果代码作为 commonjs 运行,则必须使用 requiremodule.exports。具体规则如下:

  • .ts 文件的模块系统判定方式与 .js 文件相同。要使用 importexport 语法,需要在最近的父级 package.json 中添加 type: "module"

  • .mts 文件将始终作为 esm 模块运行,类似于 .mjs 文件。

  • .cts 文件将始终作为 commonjs 模块运行,类似于 .cjs 文件。

  • .tsx 文件目前不受支持。

javascript 文件一样,在 import 语句和 import() 表达式中必须包含文件扩展名,例如:使用 import './file.ts',而不是 import './file'。出于向后兼容性考虑,在 require() 调用中也必须包含文件扩展名,例如:使用 require('./file.ts'),而不是 require('./file',这与在 commonjs 文件中 .cjs 扩展名必须在 require 调用中指定的要求类似。

tsconfig.json 中的 allowImportingTsExtensions 选项将允许 typescript 编译器 tsc 对包含 .ts 扩展名的导入说明符进行类型检查。

typescript 特性

由于 node.js 只负责移除内联类型,因此任何需要将 typescript 语法替换为新的 javascript 语法的特性都会报错,除非传入 --experimental-transform-types 标志。

--experimental-transform-types 主要需要转换的特性包括:

  • 枚举(Enum)
  • 命名空间(namespaces)
  • 传统模块(legacy module)
  • 参数属性(parameter properties)

由于装饰器(Decorators)目前是 TC39 三阶段提案,并且很快就会得到 javascript 引擎的支持,因此不会被转换,并会导致解析错误。这是一个临时限制,将在未来得到解决。

此外,node.js 不会读取 tsconfig.json 文件,也不支持依赖于 tsconfig.json 设置的特性,比如 路径别名将新版 javascript 语法转换为旧版标准

不使用 type 关键字导入类型

由于类型擦除的工作原理,使用 type 关键字对于正确擦除类型导入是必要的。如果没有 type 关键字,node.js 会将导入视为值导入,这将导致运行时错误。可以使用 tsconfigverbatimModuleSyntax 选项来匹配这种行为。

以下示例将正确运行:

ts
import type { Type1, Type2 } from './module.ts';
import { fn, type FnParams } from './fn.ts';

而以下代码将导致运行时错误:

ts
import { Type1, Type2 } from './module.ts';
import { fn, FnParams } from './fn.ts';

非文件形式的输入

类型擦除可以在 --evalSTDIN 中启用。模块系统将由 --input-type 决定,这与 javascript 的处理方式相同。需要注意的是,typescript 语法在 REPL--checkinspect 中不受支持。

源码映射

由于内联类型被替换为空白字符,同时没有新增其他额外的 javascript 代码,因此在堆栈跟踪中获得正确的行号不需要源码映射,所以 node.js 不会生成它们。当启用 --experimental-transform-types 时,源码映射默认是启用的。

依赖中的类型擦除

为了避免包作者发布用 typescript 编写的包,node.js 默认会拒绝处理 node_modules 路径下文件夹中的 typescript 文件。

路径别名

tsconfig 中可以通过配置 paths 来配置路径别名。

json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "scripts": ["./scripts/toolbox.ts"]
    }
  }
}

那么在应用中可以使用

ts
import { toolbox } from 'scripts';

来加载 monorepo 应用中的其他包。但不幸的是,node.js 并不会尊重 tsconfig 文件,因此运行时会报如下错误:

bash
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'scripts' imported from /Users/project/demo/packages/core/index.ts

node.js 提供了原生的 子路径导入 作为替换方案,需要在 package.json 中配置:

json
{
  "imports": {
    "#scripts": "scripts/toolbox.ts"
  }
}

然后在代码中这样使用:

ts
import { toolbox } from '#scripts';

注意事项

这里需要注意的是,scripts 是应用之外的包,而子路径导入的包要么位于 node_modules 目录下,要么位于应用之内。虽然可以通过将 scripts 包手动软链到 node_modules 目录下,或者将 scripts 包软链到应用之内,但这就形成了感染链,即应用依赖的 scripts 包的所有依赖模块也必须满足这个条件。

根据 CC BY-SA 4.0 许可证发布。 (95ee605)