如何在 Node.js 中原生运行 TypeScript

Node.js 23.6.0 标志着 TypeScript 开发者的一个转折点:现在你可以直接执行 .ts
文件,无需使用 ts-node 或 tsc 等转译工具。这种原生 TypeScript 支持通过消除构建步骤来简化开发工作流,同时通过 IDE 保持类型安全。
本指南涵盖了在 Node.js 中原生运行 TypeScript 所需的一切知识,从基本设置到生产环境考虑。你将学习类型剥离的工作原理、支持哪些 TypeScript 功能,以及如何配置项目以获得最佳兼容性。
关键要点
- Node.js 23.6.0 可直接执行 TypeScript 文件,无需转译
- 类型剥离移除注解的同时保留代码结构
- 大多数常见 TypeScript 功能都能正常工作,但枚举和命名空间需要变通方案
- 导入语句必须包含
.ts
扩展名 - 相比传统工具,原生执行提供 2-3 倍更快的启动时间
快速开始:在 Node.js 23.6.0 中运行 TypeScript
让我们从一个简单的 TypeScript 文件开始,演示原生执行:
// greeting.ts
function greet(name: string): string {
return `Hello, ${name}!`
}
console.log(greet("TypeScript"))
使用 Node.js 23.6.0 或更高版本,直接运行:
node greeting.ts
就这样——无需编译步骤。Node.js 会剥离类型注解并执行剩余的 JavaScript 代码。
检查你的 Node.js 版本:
node --version # 应该是 v23.6.0 或更高版本
首次运行时你会看到实验性警告:
ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
在开发中抑制此警告:
node --disable-warning=ExperimentalWarning greeting.ts
或永久设置:
export NODE_OPTIONS="--disable-warning=ExperimentalWarning"
理解 Node.js 中的类型剥离
类型剥离与传统的 TypeScript 转译有根本区别。Node.js 不是将 TypeScript 转换为 JavaScript,而是简单地移除类型注解,同时保留原始代码结构。
以下是类型剥离过程中发生的事情:
// 原始 TypeScript
function calculate(a: number, b: number): number {
return a + b
}
// 类型剥离后(Node.js 执行的内容)
function calculate(a , b ) {
return a + b
}
注意空白符的保留——这维持了准确的行号用于调试,无需源码映射。
性能优势
原生 TypeScript 执行提供显著的性能改进:
- 无转译开销:直接执行,无中间步骤
- 更快的启动时间:约 45ms 对比 ts-node 的 120ms
- 减少内存使用:内存中无需加载转译器
- 简化调试:保留原始行号
Node.js 中 TypeScript 支持的演进
了解发展历程有助于你选择正确的方法:
v22.6.0:初始类型剥离
node --experimental-strip-types app.ts
仅基本类型移除——不支持 TypeScript 特定语法。
v22.7.0:类型转换
node --experimental-transform-types app.ts
通过转换添加了对枚举和命名空间的支持。
v23.6.0:默认类型剥离
node app.ts # 默认启用类型剥离
基本 TypeScript 执行无需标志。
支持的 TypeScript 功能和限制
支持的功能
Node.js 中的原生 TypeScript 支持大多数日常 TypeScript 功能:
// ✅ 类型注解
let count: number = 0
// ✅ 接口
interface User {
id: number
name: string
}
// ✅ 类型别名
type Status = 'active' | 'inactive'
// ✅ 泛型
function identity<T>(value: T): T {
return value
}
// ✅ 类型导入
import type { Config } from './types.ts'
不支持的功能
一些 TypeScript 特定功能需要变通方案:
功能 | 支持 | 变通方案 |
---|---|---|
枚举 | ❌ | 使用联合类型或常量对象 |
命名空间 | ❌ | 使用 ES 模块 |
参数属性 | ❌ | 显式属性声明 |
JSX/TSX | ❌ | 使用传统转译 |
装饰器 | ❌ | 等待 V8 支持 |
变通方案示例:
// ❌ 枚举(不支持)
enum Status {
Active,
Inactive
}
// ✅ 联合类型替代方案
type Status = 'active' | 'inactive'
// ✅ 常量对象替代方案
const Status = {
Active: 'active',
Inactive: 'inactive'
} as const
Discover how at OpenReplay.com.
文件扩展名和模块系统
Node.js 使用文件扩展名来确定模块处理方式:
.ts
- 遵循 package.json 中的"type"
字段(ESM 或 CommonJS).mts
- 始终作为 ESM 处理.cts
- 始终作为 CommonJS 处理
重要提示:本地导入必须包含 .ts
扩展名:
// ❌ 传统 TypeScript
import { utils } from './utils'
// ✅ Node.js 原生 TypeScript
import { utils } from './utils.ts'
为原生 TypeScript 配置 tsconfig.json
虽然 Node.js 在执行期间不读取 tsconfig.json,但正确的配置确保 IDE 支持和类型检查:
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"moduleResolution": "nodenext",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": false,
"verbatimModuleSyntax": true,
"strict": true,
"noEmit": true
}
}
关键设置说明:
allowImportingTsExtensions
:允许导入路径中的.ts
rewriteRelativeImportExtensions
:设为 false 以保留.ts
扩展名verbatimModuleSyntax
:强制显式类型导入noEmit
:防止意外生成 JavaScript
单独运行类型检查:
tsc --noEmit
从传统 TypeScript 工具的迁移指南
从 ts-node 迁移
- 移除 ts-node 依赖:
npm uninstall ts-node
- 更新 package.json 脚本:
// 之前
"scripts": {
"dev": "ts-node src/index.ts"
}
// 之后
"scripts": {
"dev": "node src/index.ts"
}
- 处理不支持的功能(将枚举转换为联合类型)
从 tsc 编译迁移
- 移除构建步骤:
// 移除这些脚本
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
// 改用这个
"scripts": {
"start": "node src/index.ts"
}
- 更新 CI/CD 流水线以跳过编译
性能对比
原生 TypeScript 执行显示显著改进:
指标 | 原生 TypeScript | ts-node | tsc + node |
---|---|---|---|
启动时间 | ~45ms | ~120ms | ~200ms |
内存使用 | 基准 | +30MB | +10MB |
构建步骤 | 无 | 无 | 必需 |
常见陷阱和解决方案
导入路径错误
// 错误:Cannot find module './utils'
import { helper } from './utils'
// 解决方案:包含 .ts 扩展名
import { helper } from './utils.ts'
不支持的语法
// 错误:不支持枚举
enum Color { Red, Blue }
// 解决方案:使用常量对象
const Color = { Red: 0, Blue: 1 } as const
类型检查混淆
记住:Node.js 不执行类型检查。始终运行 tsc --noEmit
进行类型验证。
生产环境考虑
何时使用原生 TypeScript
- 开发环境
- 原型和实验
- 小型脚本和工具
- 准备使用实验性功能的团队
何时避免使用
- 生产应用(直到稳定)
- 需要完整 TypeScript 功能的项目
- 需要稳定工具的团队
- 复杂的构建流水线
Docker 调整
# 之前
FROM node:23-alpine
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
# 之后
FROM node:23-alpine
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "src/index.ts"]
结论
Node.js 23.6.0 中的原生 TypeScript 支持代表了开发工作流的显著简化。通过消除转译步骤,你可以专注于编写代码而不是配置构建工具。虽然存在限制——特别是围绕 TypeScript 特定语法——但对于大多数开发场景来说,其优势是令人信服的。
从在开发环境中尝试原生 TypeScript 开始,逐步迁移现有项目,并为 TypeScript 在 JavaScript 运行的任何地方都能运行的未来做好准备。随着此功能的稳定,期待在即将发布的 Node.js 版本中有更广泛的采用和扩展的功能。
常见问题
虽然技术上可行,但还不建议在生产环境中使用。该功能仍处于实验阶段,可能会发生变化。将其用于开发环境、原型和非关键应用。对于生产环境,继续使用传统转译,直到该功能变得稳定。
是的,你应该保留 TypeScript 作为开发依赖项用于类型检查。Node.js 只剥离类型而不验证它们。在开发期间或在 CI 流水线中单独运行 tsc --noEmit 来捕获类型错误。
原生 TypeScript 需要在导入路径中使用显式的 .ts 扩展名,不像传统的 TypeScript 工具。将所有本地导入从 './module' 更新为 './module.ts'。外部包导入不需要扩展名。
对于简单情况,用联合类型替换枚举,或者对于数字枚举使用带有 as const 断言的常量对象。对于字符串枚举,联合类型完美适用。这种方法实际上更利于 tree-shaking,并且与现代 TypeScript 实践更好地对齐。
Node.js 团队专注于类型剥离而不是完整转译,出于性能考虑。需要运行时转换的功能,如枚举和装饰器,可能在 V8 原生实现它们时获得支持,但目标是快速、最小处理,而不是完整的 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.