首页 / Node.js 教程 / 原生运行 TypeScript

Node.js 教程

原生运行 TypeScript

本教程共 76 篇 · 第 62 篇 · 更新于 2026-07-25 · 约 7 分钟阅读

Node.jsTypeScripttype strippingtsconfig类型

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 执行。

Tip

v24 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.readFilehttp.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 官方也不鼓励。正确做法是:

  1. tsc.ts 编译成 .js 发布;
  2. 同时生成 .d.ts 类型声明作为「副驾文件」随包发布,让消费者既有能跑的 JS,又有类型提示;
  3. package.jsonmain 指向编译后的 .jstypes 指向 .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。发布时记得用 .npmignorefiles 字段把测试、源码排除掉,只发编译产物和 .d.ts

一句话总结思路

原生 TS 运行让「开发循环」变短了:写 .ts、直接 node 跑、用 tsc --noEmit 查类型。只要守住「只用可擦除语法」这条线,大多数项目从此告别编译步骤。需要重型特性时,再上 tsx--experimental-transform-types 也不迟。