Modules: TypeScript
致读者
本译文在忠于原文的基础上,融入了译者基于自身领域知识所添加的解释性内容和本地化表达。这些精心添加的内容旨在增进读者对原文核心信息的理解。如对内容有任何疑问,欢迎您参与评论区讨论,或查阅原文以作参考。
启用
在 node.js
中启用运行时 typescript
支持有两种方式:
- 如果需要完整支持所有
typescript
的语法特性,包括使用任意版本的typescript
,可以使用第三方包。 - 如果只需要轻量级支持,可以使用
node.js
内置的类型擦除功能。
完整 typescript
支持
可以通过第三方包来获得完整的 typescript
支持(包括对 tsconfig.json
的配置支持)。这里以 tsx
为例进行说明,当然市面上还有许多类似的库可供选择。
首先,需要使用项目中的包管理器将其安装为开发依赖。比如使用 npm
:
npm install --save-dev tsx
然后,可以通过以下方式运行 typescript
代码:
npx tsx your-file.ts
或者,通过 node
命令运行:
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.js
的 v22.7.0
版本中实验性支持。
需要注意的是,依赖于 tsconfig.json
设置的 typescript
特性
- 路径映射功能:
tsconfig
配置文件中的paths
字段不会被转换,因此会产生错误。最接近的可用特性是子路径导入,限制是路径需要以#
开头。 - 将新版本的
javascript
语法转换为旧版本的功能。
是特意不被支持的。如果需要完整的 typescript
支持,请参考上文的 完整 typescript
支持 说明。
类型移除特性的 设计理念是保持轻量级,通过以下两个核心策略实现这一目标:
- 有意不支持需要额外生成
javascript
代码的类型语法。 - 将内联类型语法替换为空白字符。
由于上述两个核心策略,对于源码映射的影响基本是微乎其微,node.js
不需要再额外维护源映射。
当启用
--experimental-transform-types
时,源映射默认开启。
虽然类型擦除功能与大多数版本的 typescript
都兼容,但建议使用 5.7
或更新版本,并使用以下 tsconfig.json
配置:
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true
}
}
确定模块系统
node.js
在 typescript
文件中同时支持 commonjs
和 esm
语法。需要注意的是,node.js
不会在这两种模块系统之间进行转换。如果代码作为 esm
模块运行,就必须使用 import
和 export
语法;如果代码作为 commonjs
运行,则必须使用 require
和 module.exports
。具体规则如下:
.ts
文件的模块系统判定方式与.js
文件相同。要使用import
和export
语法,需要在最近的父级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
会将导入视为值导入,这将导致运行时错误。可以使用 tsconfig
的 verbatimModuleSyntax
选项来匹配这种行为。
以下示例将正确运行:
import type { Type1, Type2 } from './module.ts';
import { fn, type FnParams } from './fn.ts';
而以下代码将导致运行时错误:
import { Type1, Type2 } from './module.ts';
import { fn, FnParams } from './fn.ts';
非文件形式的输入
类型擦除可以在 --eval
和 STDIN
中启用。模块系统将由 --input-type
决定,这与 javascript
的处理方式相同。需要注意的是,typescript
语法在 REPL
、--check
和 inspect
中不受支持。
源码映射
由于内联类型被替换为空白字符,同时没有新增其他额外的 javascript
代码,因此在堆栈跟踪中获得正确的行号不需要源码映射,所以 node.js
不会生成它们。当启用 --experimental-transform-types
时,源码映射默认是启用的。
依赖中的类型擦除
为了避免包作者发布用 typescript
编写的包,node.js
默认会拒绝处理 node_modules
路径下文件夹中的 typescript
文件。
路径别名
tsconfig
中可以通过配置 paths
来配置路径别名。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"scripts": ["./scripts/toolbox.ts"]
}
}
}
那么在应用中可以使用
import { toolbox } from 'scripts';
来加载 monorepo
应用中的其他包。但不幸的是,node.js
并不会尊重 tsconfig
文件,因此运行时会报如下错误:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'scripts' imported from /Users/project/demo/packages/core/index.ts
node.js
提供了原生的 子路径导入 作为替换方案,需要在 package.json
中配置:
{
"imports": {
"#scripts": "scripts/toolbox.ts"
}
}
然后在代码中这样使用:
import { toolbox } from '#scripts';
注意事项
这里需要注意的是,scripts
是应用之外的包,而子路径导入的包要么位于 node_modules
目录下,要么位于应用之内。虽然可以通过将 scripts
包手动软链到 node_modules
目录下,或者将 scripts
包软链到应用之内,但这就形成了感染链,即应用依赖的 scripts
包的所有依赖模块也必须满足这个条件。