构建脚本 import .ts 报 ERR_UNKNOWN_FILE_EXTENSION,用 --experimental-strip-types 一行解决
写了一个数据预生成脚本(.mjs),需要读项目里用 TypeScript 维护的静态配置——毕竟那是唯一真相源,不想复制一份 .json 出来让数据漂移。结果 node scripts/gen.mjs 一跑:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"
直觉上以为这是 ESM 的某个配置问题,折腾 tsconfig、moduleResolution 半天,无果。
现象
构建期脚本(.mjs)尝试 import 项目里的 TypeScript 数据模块:
// scripts/gen-tiers.mjs
import { REGIONS } from '../src/data/regions.ts'Node 直接报错退出,无论是 import() 动态导入还是静态 import,都是同一个错:
ERR_UNKNOWN_FILE_EXTENSION: Unknown file extension ".ts" for .../src/data/regions.ts
换成 require('./regions.ts') 也不行——ESM 脚本根本不能用 require,而且 Node 的 CommonJS loader 同样不认识 .ts。
根因
Node.js 运行时没有内置 TypeScript 解析能力。它只认 .js/.mjs/.cjs/.json 等原生格式,遇到 .ts 扩展名直接拒绝,不管文件内容是什么。
常见的将就办法有两种:
- 复制数据到
.json/.mjs——维护两份,一旦.ts改了忘记同步就漂移。 - 上
tsx/esbuild/jiti——多引入一个运行时依赖,package.json变重。
如果被 import 的 .ts 文件内部有值层面的相对 import 且没有写扩展名(例如 import { Foo } from './schema'),Node 不会自动补 .ts——这条 import 会再次报 ERR_UNKNOWN_FILE_EXTENSION。
只有 import type { Foo } from './schema' 这样的纯类型 import 才会被整条擦除,运行时对 ./schema 不产生任何真实依赖。所以数据模块顶部的类型引用务必用 import type,不能用值 import。
正解
Node 22.6+ 实验性地内置了 TypeScript 类型擦除功能,22.18+ 趋于稳定。加一个 flag 即可让 .mjs 脚本直接 import .ts 文件:
node --experimental-strip-types scripts/gen-tiers.mjspackage.json 里写成 script:
{
"scripts": {
"gen": "node --experimental-strip-types scripts/gen-tiers.mjs"
}
}脚本里 import 时必须显式带 .ts 扩展名,Node 不像打包器那样自动补全:
// scripts/gen-tiers.mjs
import { REGIONS } from '../src/data/regions.ts' // ✅ 必须写 .ts
import { DATASET_VERSION } from '../src/data/schema.ts' // ✅原理:--experimental-strip-types 只做类型语法删除,不做类型检查,也不需要 tsconfig.json。它注册了一个 .ts loader,遇到类型注解直接抹掉,运行时拿到的是干净的 JS。因此速度极快,零额外依赖。
快速验证:
node --experimental-strip-types --input-type=module \
-e "import('./src/data/regions.ts').then(m => console.log('OK', m.REGIONS.length))"
# 打印 OK 42 即通node --experimental-strip-types scripts/gen.mjs 无需额外依赖,数据 .ts 只维护一份,构建脚本直接 import,漂移风险归零。Node 22.18+ 可放心使用,老版本退回 tsx 或 jiti。
一句话外卖
构建脚本需要读 TypeScript 静态数据时,加
--experimental-strip-types让 Node 直接吃.ts,比维护两份数据的成本低得多——前提是.ts里的跨模块引用必须用import type或带扩展名。