原生运行 TypeScript
本教程共 76 篇 · 第 62 篇 · 更新于 2026-07-25 · 约 7 分钟阅读
62. 原生运行 TypeScript
本节目标:-
node example.ts直接跑 TS 的原理:type stripping - v22.18+ / v24 的默认行为,以及老版本的--experimental-strip-types- 哪些 TS 语法能跑、哪些不行(重点讲限制) - 配tsconfig.json、装@types/node、用tsc --noEmit做类型检查 - 发布 TS 包时类型声明怎么处理
TypeScript 给 JavaScript 加了类型,能让你在写代码时(而不是运行时)就逮住一堆低级错误。但长期以来有个麻烦:TypeScript 不能直接跑,你得先用 tsc 编译成 JavaScript,再拿 Node.js 去跑那份编译产物。开发时每改一行就要编译一次,很打断思路。
好消息是自 v22.18.0 起,Node.js 内置了「类型剥离」(type stripping)能力——它能直接吃下 .ts 文件,把类型注解这类「运行时不需要的语法」剥掉,剩下的纯 JavaScript 直接执行。v24 LTS 里这是默认行为,连标志都不用加。这一章就把这件事讲透:怎么跑、有什么限制、怎么配 tsconfig、类型声明从哪来。
TypeScript 是什么
TypeScript 是微软维护的开源语言,可以理解为「带类型的 JavaScript」。它在 JS 之上加了一套类型语法:
type User = {
name: string;
age: number;
};
function isAdult(user: User): boolean {
return user.age >= 18;
}
const justine = { name: 'Justine', age: 23 } satisfies User;
const result = isAdult(justine);
类型分两部分:一是代码本身(带类型注解的 JS),二是类型定义(.d.ts 描述现有 JS 的形状)。编译时,类型相关语法会被全部删掉,留下干净的 JS 在任何环境运行。
直接跑起来
从 v22.18.0 开始,只要你的 TS 只用了「可擦除语法」,直接:
node example.ts
如果是更早的版本(v22.18 之前),需要显式加标志:
node --experimental-strip-types example.ts
就这么一行命令,不需要装 ts-node、不需要 tsc 先编译。Node.js 内部的 Amaro 加载器会剥掉类型,把剩余的 JS 喂给 V8 执行。
Tipv24 LTS 是默认开启的。如果你在老环境里跑,不确定版本,用
node --version确认。低于 v22.18 又没加标志,node example.ts会报错而不是自动剥离。
关键认知:它不做类型检查
这是最容易误解的一点。Node.js 跑 .ts 文件,只是「剥类型后执行」,不会报告类型错误。下面这段代码能照常运行,尽管 age 明明是字符串:
type User = { name: string; age: number };
function isAdult(user: User): boolean {
return user.age >= 18;
}
// 故意传错类型,运行时照样不报错
isAdult({ name: 'Tom', age: 'secret' as any });
所以正确的日常姿势是「两条腿走路」:
- 开发时直接
node example.ts拿快速反馈,改完立刻看运行结果; - 类型检查交给
tsc,在单独命令或 CI 里跑:
npx tsc --noEmit
--noEmit 表示只检查、不产出 JS 文件。这样你既有 TS 的类型安全感,又有 Node 原生运行的轻快感。
限制:什么不能跑
type stripping 只处理「删了也不影响运行时」的语法。凡是需要生成 JS 代码才能实现的 TS 特性,它都搞不定。这几点务必记牢:
不能用的特性
enum:编译后会产生真实对象,剥离不掉- 参数属性(parameter properties):
constructor(private x: number)这种,要生成赋值代码 - 含运行时代码的
namespace - 导入别名(import aliases):
import { foo as bar }在某些形式下需要改写
如果你写了这些,Node.js 会直接报错,提示你该用 --experimental-transform-types 或者改用 runner。
Warning想彻底避免踩雷,在
tsconfig.json里开erasableSyntaxOnly(TypeScript 5.8+)。它会让tsc在遇到不可剥离的语法时直接报错,从源头保证你的代码能被 Node 原生跑。这是「写 TS 又想免编译器」最稳的配置。
能用不可剥离语法怎么办
要么改写代码(比如用 const enum 换成普通 const 对象、用 interface 替代 enum),要么用 --experimental-transform-types 标志让 Node 顺手帮你转译:
node --experimental-transform-types example.ts
注意这不是默认开启的,且有一定性能开销。能用可擦除语法就尽量用,别依赖这个开关。
配置 tsconfig.json
Node 的 TS 加载器不读 tsconfig.json 来决定怎么跑——它只做剥离。但 tsconfig.json 对你和 tsc 依然重要:它让编辑器和类型检查器知道「按 Node 的方式」来理解你的代码。官方建议的 compilerOptions 大致长这样:
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true
},
"include": ["**/*.ts"]
}
几个关键项:
target/module:对齐你运行的 Node 版本,v24 用es2022/node16没问题。strict:打开所有严格检查,TS 的价值大半在这里。erasableSyntaxOnly:前面说过,保证代码可被原生跑。noEmit:因为我们让 Node 直接跑源文件,不需要tsc产出 JS。
Note建议 TypeScript 版本用 5.7 以上,并让编辑器的 TS 版本和项目一致,否则可能出现「编辑器不报错、Node 跑不了」或反之的错位。
类型声明:@types/node
你写的 TS 要用 fs.readFile、http.createServer 这些 Node API,TS 得知道它们的类型。答案就是 @types/node:
npm install --save-dev @types/node
装了之后,编辑器里敲 fs. 会蹦出补全,传错参数 TS 也会当场拦你。大量第三方库的类型也发布在 @types/* 命名空间下(由 DefinitelyTyped 社区维护),npm install -D @types/express 这种就行。
用 runner 跑更重的 TS
如果你的项目就是离不开 enum、装饰器这类需要转译的特性,或者你在 v22.7.0 之前的老版本上,可以用 runner 代劳。两个主流选择:
# ts-node:默认带类型检查
npm install --save-dev ts-node
npx ts-node example.ts
# tsx:更快,但不做类型检查(记得另外跑 tsc)
npm install --save-dev tsx
npx tsx example.ts
# 或者挂到 node 上:
node --import=tsx example.ts
tsx 现在很受欢迎,启动快、对 ESM 友好。但要注意它不查类型,类型安全还是得靠 tsc --noEmit 兜底。
发布 TypeScript 包
如果你做个库给别人用,发布到 npm 时有个重要约定:别直接发布 .ts 源码让消费者去跑。原因是 Node 默认不对 node_modules 里的文件做类型剥离(会拖慢 tsc 和编辑器),TS 官方也不鼓励。正确做法是:
- 用
tsc把.ts编译成.js发布; - 同时生成
.d.ts类型声明作为「副驾文件」随包发布,让消费者既有能跑的 JS,又有类型提示; package.json的main指向编译后的.js,types指向.d.ts。
my-ts-pkg/
├── main.js // 编译产物
├── main.d.ts // 类型声明
├── package.json // "main": "main.js", "types": "main.d.ts"
└── src/main.ts // 源码,开发用
npm publish 触发前会先跑 prepack,你在这个钩子里执行 tsc 生成声明文件即可。类型声明是「确定性」产物,不需要提交进 git,构建时生成就好。
Tip测试文件可以用源文件的扩展名,比如
"test": "node --test './src/**/*.test.ts'",因为 Node 能直接跑 TS 测试了;而main指向的必须是编译后的.js。发布时记得用.npmignore或files字段把测试、源码排除掉,只发编译产物和.d.ts。
一句话总结思路
原生 TS 运行让「开发循环」变短了:写 .ts、直接 node 跑、用 tsc --noEmit 查类型。只要守住「只用可擦除语法」这条线,大多数项目从此告别编译步骤。需要重型特性时,再上 tsx 或 --experimental-transform-types 也不迟。