TypeScript 在 Node 中的实用配置
你已经在浏览器端编写 TypeScript。现在你需要在服务器端运行它——用于 API、构建脚本或 SSR。问题在于:大多数配置指南已经过时,推荐的是 CommonJS 配置或与现代 Node.js 不匹配的工具。
本指南涵盖了 TypeScript Node.js 配置的两种方法:使用 tsc 编译并运行 JavaScript,或使用 Node 的原生类型剥离直接运行 .ts 文件。两种方法都可行。各自适用于不同的场景。
核心要点
- 在 package.json 中设置
"type": "module"以默认启用 ESM,适用于现代 TypeScript Node.js 项目 - 对于生产部署、发布的包以及使用枚举、命名空间或参数属性的代码,使用
tsc编译 - 对于本地脚本、开发服务器和快速原型,使用 Node 的原生类型剥离
- 始终对仅类型导入使用
import type,以避免类型剥离时的运行时错误 - 在 CI 中运行
tsc --noEmit,因为 Node 的类型剥离不执行类型检查
基础配置:Node 24 LTS 和 ESM
从这个基础开始:
{
"type": "module"
}
这将默认启用 ESM。你的导入使用 ESM 语法,Node 会相应地解析模块。
Node 24 是此配置的当前 LTS 基线(可从此处下载:https://nodejs.org/en/download)。
方法 1:使用 tsc 编译,运行 JavaScript
这种方法将编译与执行分离。适用于生产部署、发布的包,或当你需要完整的 TypeScript 特性支持时。
适用于 Node 24 的 tsconfig
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"lib": ["ES2024"]
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
此配置中的关键设置:
module: NodeNext和moduleResolution: NodeNext:匹配 Node 的实际模块解析行为verbatimModuleSyntax:要求对仅类型导入显式使用import type——这对避免运行时错误至关重要(参见 TypeScript 文档:https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax)isolatedModules:确保与单文件转译工具的兼容性
脚本配置
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc --watch"
}
}
运行 npm run build,然后运行 npm start。编译后的 JavaScript 位于 dist/ 目录中。
方法 2:Node 的原生 TypeScript(类型剥离)
Node 在 Node 22 中引入了原生 TypeScript 支持,并在 Node 24 LTS 中通过类型剥离将其稳定化。适用于脚本、本地工具或开发环境,当你希望零构建步骤时。
官方文档:https://nodejs.org/api/typescript.html
工作原理
在 Node 24+ 中,直接运行:
node src/index.ts
(旧版 Node 需要实验性标志;Node 24 不需要。)
关键限制
Node 的原生 TypeScript 仅剥离类型——它不进行类型检查。你仍然需要在 CI 或编辑器中使用 tsc --noEmit 来捕获错误。
其他限制:
- 忽略 tsconfig.json:Node 不读取你的编译器选项
- 需要显式文件扩展名:即使源文件是
utils.ts,也要写import { foo } from './utils.js' - 遵守 ESM vs CJS 规则:你的
package.jsontype 字段很重要 - 不会运行 node_modules 中的 TypeScript:依赖项必须是编译后的 JavaScript
- 仅支持可擦除语法:枚举、命名空间和参数属性会失败,除非启用
--experimental-transform-types
文件扩展名很重要
对于混合模块格式:
.mts文件 → 始终是 ESM.cts文件 → 始终是 CommonJS.ts文件 → 遵循package.jsontype 字段
Discover how at OpenReplay.com.
避免运行时错误
对仅类型导入使用 import type:
// 正确
import type { Request, Response } from 'express'
import express from 'express'
// 错误 - 使用类型剥离时会在运行时失败
import { Request, Response } from 'express'
在 tsconfig 中启用 verbatimModuleSyntax 以在开发期间捕获这些问题,即使 Node 在运行时会忽略该配置。
使用哪种方法
使用 tsc 编译的场景:
- 生产部署
- 发布的 npm 包
- 使用枚举、命名空间或参数属性的代码
- 需要在生产环境中使用 source maps 的项目
使用原生类型剥离的场景:
- 本地脚本和工具
- 开发服务器(配合
--watch使用) - 快速原型
- SSR 开发构建
实用的开发配置
结合两种方法:
{
"scripts": {
"dev": "node --watch src/index.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
"start": "node dist/index.js"
}
}
开发环境使用原生执行以提高速度。CI 运行 typecheck。生产环境部署编译后的 JavaScript。
结论
现代 TypeScript Node.js 配置比旧指南所建议的更简单。使用 ESM,配置 NodeNext 模块解析,并根据上下文选择执行策略。原生类型剥离适用于开发和脚本。编译输出适用于生产和包。两种方法共享相同的源代码和 tsconfig——你不会被锁定在任何一种方法中。
常见问题
Node 的模块解析在 ESM 中需要显式的文件扩展名。当你写 import from ./utils.js 时,Node 在运行时会查找该确切路径,即使你的源文件是 utils.ts。由于类型剥离会移除类型但不会重命名文件,而 tsc 会输出 .js 文件,因此在源代码中使用 .js 扩展名可确保导入在两种场景下都能正常工作。
默认情况下不可以。枚举需要代码转换,而不仅仅是类型移除。你可以启用 --experimental-transform-types 标志来支持枚举、命名空间和参数属性,但这会增加复杂性。对于更简单的配置,可以考虑使用带有 as const 断言的常量对象作为枚举的替代方案。
对于大多数用例,不需要。Node 24 的原生类型剥离可以处理直接的 .ts 执行。像 ts-node 和 tsx 这样的工具是可选的便利工具,它们添加了 tsconfig.json 支持、路径别名解析和完整的 TypeScript 转换而无需标志。只有在你的配置需要这些功能时才使用它们。
使用原生类型剥离时,Node 直接执行你的 .ts 文件,因此堆栈跟踪中的行号与你的源代码匹配。对于编译后的代码,在 tsconfig.json 中启用 sourceMap,Node 会自动使用 .js.map 文件在错误和调试器会话中显示原始 TypeScript 位置。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.