用 tsconfig 拦住 enum 与 parameter properties:Node type stripping 的配置侧写法
type stripping 做什么,不做什么
Node 内置的 TypeScript 支持只做一件事:把可擦除(erasable)的 TS 语法替换成空白。类型注解、interface、type 声明被抹掉,剩下的代码按原样执行。它不做类型检查,也不生成 source map。
这意味着配置的目标很明确——保证源码里只出现能被擦除的语法,其余问题交给 tsc 单独校验。官方推荐 TypeScript 5.8+。
推荐的 compilerOptions
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
}
}
各选项的作用如下。
erasableSyntaxOnly:让 tsc 提前拦住不可擦除语法
这是配置侧最关键的一项。打开后,源码里一旦出现 Node 无法擦除的语法,tsc 直接报错,而不是等到 node 跑起来才崩。覆盖的正是 enum、parameter properties、带 runtime code 的 namespace、import alias 这几类。
verbatimModuleSyntax:强制写 import type
Node 剥离类型时只看语法,无法判断某个 import 是不是只在类型位置使用。没加 type 修饰的导入会被当成值导入,运行时找不到对应导出就报错。打开 verbatimModuleSyntax 会强制类型导入必须写成 import type,与 Node 的剥离行为对齐。
allowImportingTsExtensions 与 rewriteRelativeImportExtensions
allowImportingTsExtensions 允许 import 说明符里带 .ts 扩展名,这样 tsc 才能直接吃源码做类型检查(该选项需配合 noEmit 或不产出 JS 的模式)。Node 在 import / import() 里强制要求写扩展名,require() 出于兼容同样强制。
rewriteRelativeImportExtensions 管输出侧:编译时把 .ts 说明符改写成 .js。
noEmit、target、module
noEmit 只在仓库里执行 *.ts(例如构建脚本)时需要;如果产物是分发出去的 *.js,不必打开。target 用 esnext,module 用 nodenext。
运行时会报 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 的语法
Node 遇到以下不可擦除语法会直接抛 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX:
Enum声明- 带 runtime code 的
namespace - parameter properties(构造函数参数属性)
- import aliases
不带 runtime code 的 namespace(只有 export type)可以用。Decorators 属于 TC39 Stage 3,Node 既不转换也不 polyfill,直接抛 parser error。
Node 不读 tsconfig.json
tsconfig.json 对 Node 运行时不可见。paths 别名不会被转换成实际路径,遇到直接报错。替代方案是使用必须以 # 开头的 subpath imports,在 package.json 的 imports 字段里声明映射。
扩展名与模块系统
.ts 的模块判定规则和 .js 相同:要走 ESM 就需要 package.json 里写 "type": "module"。.mts 恒为 ESM,.cts 恒为 CJS,.tsx 不支持。Node 不会在 CJS 和 ESM 之间做转换。
CI 里用 tsc --noEmit 单独做类型检查
类型检查从运行链路里拆出来,交给 CI 里的 tsc:
{
"scripts": {
"typecheck": "tsc --noEmit",
"start": "node src/index.ts",
"test": "node --test"
}
}
# .github/workflows/ci.yml
- run: npm ci
- run: npx tsc --noEmit
- run: node --test
node 负责执行,tsc --noEmit 负责类型与可擦除性校验,两者职责不重叠。erasableSyntaxOnly 报出的错误会在这一步挡住合并。
需要完整支持时用 tsx
需要 decorators、enum 这类非擦除语法,或者想要更完整的转换能力,可以换成 tsx:
npm install --save-dev tsx
npx tsx your-file.ts
# 或者
node --import=tsx your-file.ts
链接
- Node.js TypeScript 文档:https://nodejs.org/api/typescript.html
- tsx:https://tsx.is