返回博客

构建脚本 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 的某个配置问题,折腾 tsconfigmoduleResolution 半天,无果。

现象

构建期脚本(.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 扩展名直接拒绝,不管文件内容是什么。

常见的将就办法有两种:

  1. 复制数据到 .json / .mjs——维护两份,一旦 .ts 改了忘记同步就漂移。
  2. tsx / esbuild / jiti——多引入一个运行时依赖,package.json 变重。
坑:import type 擦除后仍可能触发隐式依赖解析失败

如果被 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.mjs

package.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 22.6+ 一个 flag,构建脚本直接复用 .ts SSoT

node --experimental-strip-types scripts/gen.mjs 无需额外依赖,数据 .ts 只维护一份,构建脚本直接 import,漂移风险归零。Node 22.18+ 可放心使用,老版本退回 tsxjiti

一句话外卖

构建脚本需要读 TypeScript 静态数据时,加 --experimental-strip-types 让 Node 直接吃 .ts,比维护两份数据的成本低得多——前提是 .ts 里的跨模块引用必须用 import type 或带扩展名。