12k
All articles

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

无需转译工具即可在 Node.js 中原生运行 TypeScript,涵盖类型剥离机制、支持的语法、tsconfig 配置及从 ts-node 迁移的步骤。

OpenReplay Team
OpenReplay Team
如何在 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

文件扩展名和模块系统

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 迁移

  1. 移除 ts-node 依赖:
npm uninstall ts-node
  1. 更新 package.json 脚本:
// 之前
"scripts": {
  "dev": "ts-node src/index.ts"
}

// 之后
"scripts": {
  "dev": "node src/index.ts"
}
  1. 处理不支持的功能(将枚举转换为联合类型)

从 tsc 编译迁移

  1. 移除构建步骤:
// 移除这些脚本
"scripts": {
  "build": "tsc",
  "start": "node dist/index.js"
}

// 改用这个
"scripts": {
  "start": "node src/index.ts"
}
  1. 更新 CI/CD 流水线以跳过编译

性能对比

原生 TypeScript 执行显示显著改进:

指标原生 TypeScriptts-nodetsc + 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 版本中有更广泛的采用和扩展的功能。

常见问题

我可以在 Node.js 23.6.0 的生产环境中使用原生 TypeScript 吗?

虽然技术上可行,但还不建议在生产环境中使用。该功能仍处于实验阶段,可能会发生变化。将其用于开发环境、原型和非关键应用。对于生产环境,继续使用传统转译,直到该功能变得稳定。

我仍然需要将 TypeScript 安装为依赖项吗?

是的,你应该保留 TypeScript 作为开发依赖项用于类型检查。Node.js 只剥离类型而不验证它们。在开发期间或在 CI 流水线中单独运行 tsc --noEmit 来捕获类型错误。

为什么从 ts-node 迁移时我的导入失败了?

原生 TypeScript 需要在导入路径中使用显式的 .ts 扩展名,不像传统的 TypeScript 工具。将所有本地导入从 './module' 更新为 './module.ts'。外部包导入不需要扩展名。

由于不支持枚举,我该如何处理它们?

对于简单情况,用联合类型替换枚举,或者对于数字枚举使用带有 as const 断言的常量对象。对于字符串枚举,联合类型完美适用。这种方法实际上更利于 tree-shaking,并且与现代 TypeScript 实践更好地对齐。

原生 TypeScript 支持最终会包含所有 TypeScript 功能吗?

Node.js 团队专注于类型剥离而不是完整转译,出于性能考虑。需要运行时转换的功能,如枚举和装饰器,可能在 V8 原生实现它们时获得支持,但目标是快速、最小处理,而不是完整的 TypeScript 兼容性。

Open-source session replay

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.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.