Node.js 24 與 TypeScript:2026 年免建置開發真的可行嗎?
Node.js 24 正式將 TypeScript 的 type stripping 從 experimental 標記為穩定功能。這代表你可以直接 node app.ts 而不需要任何編譯步驟。但「可以跑」和「production 可用」之間還有一段距離。這篇文章從實務角度拆解:哪些情境可以真的免建置、哪些仍然需要 tsc 或 bundler、以及團隊該怎麼規劃遷移。
先講結論
- 可以免建置的情境 — CLI 工具、腳本、cron job、內部 API 服務、開發階段的快速原型。這些場景用
node app.ts直接跑,開發體驗大幅提升。 - 仍然需要建置的情境 — 需要
enum、namespace、decorator(舊語法)、const enum等需要語義轉換的 TypeScript 功能;需要產出 npm 套件供他人使用;前端打包。 - 團隊遷移策略 — 從開發腳本和測試開始,逐步擴大到內部服務,最後才考慮 production API。不要一步到位。
Node.js 24 的 TypeScript 支援到底做了什麼
Node.js 的 TypeScript 支援採用的是 type stripping(型別剝離)策略,而不是完整的 TypeScript 編譯。理解這個區別很重要,因為它直接決定了哪些功能能用、哪些不能用。
Type stripping 的工作方式很簡單:在執行前,Node.js 使用 @anthropic-ai/swc 的輕量版本,把 TypeScript 原始碼中的型別標註「擦除」成空白,保留原始的 JavaScript 程式碼結構。這意味著:
- 不做型別檢查 — Node.js 不會檢查你的型別是否正確。它只是把型別標註移除,然後執行剩下的 JavaScript。型別檢查仍然是 IDE 和 CI 中
tsc --noEmit的工作。 - 不做語法轉換 — 任何需要改變 JavaScript 輸出結構的 TypeScript 功能都不支援。型別標註可以被無痛移除,但
enum需要被轉換成物件,這就超出 type stripping 的範圍了。 - source map 自動對應 — 因為只是擦除型別(用空白取代),行號和列號不會改變,所以錯誤堆疊追蹤天然就是準確的,不需要額外的 source map。
# Node.js 24 直接執行 TypeScript
# 不需要任何 flag — 已是穩定功能
node app.ts
# 搭配 watch mode 做開發
node --watch app.ts
# 搭配內建測試執行器
node --test tests/*.ts可以免建置直接跑的 TypeScript 功能
以下這些 TypeScript 功能都是「純型別標註」,可以被安全地剝離而不影響程式行為:
型別標註與介面
// 全部可以直接 node app.ts 執行
// 型別標註
const port: number = 3000;
const name: string = "nodejs-tw";
// 介面定義
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// 泛型函式
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// type alias
type RequestHandler = (req: Request, res: Response) => Promise<void>;
// union / intersection types
type Result<T> = { ok: true; data: T } | { ok: false; error: string };
// utility types
type UserDTO = Pick<User, "id" | "name">;
type PartialUser = Partial<User>;型別斷言與型別守衛
// as 斷言 — 剝離後就是普通賦值
const input = event.body as string;
// 型別守衛 — 函式本體是純 JavaScript,型別標註被剝離
function isUser(obj: unknown): obj is User {
return typeof obj === "object" && obj !== null && "id" in obj;
}
// satisfies 運算子 — 純型別層面,直接剝離
const config = {
port: 3000,
host: "localhost",
} satisfies ServerConfig;類別中的型別標註
// class 中的型別標註可以被剝離
class UserService {
private db: Database; // 型別標註被剝離
readonly logger: Logger; // readonly 修飾詞被剝離
constructor(db: Database, logger: Logger) {
this.db = db;
this.logger = logger;
}
async getUser(id: string): Promise<User | null> { // 回傳型別被剝離
return this.db.query("SELECT * FROM users WHERE id = $1", [id]);
}
}仍然需要 tsc / bundler 的功能
以下功能會讓 Node.js 的 type stripping 失敗或產生錯誤行為,因為它們需要語義層面的轉換:
enum(列舉)
// 不能直接用 node 跑 — enum 需要被轉換成 JavaScript 物件
enum Status {
Active = "active",
Inactive = "inactive",
Suspended = "suspended",
}
// 替代方案:用 as const 物件(可以被 type stripping 處理)
const Status = {
Active: "active",
Inactive: "inactive",
Suspended: "suspended",
} as const;
type Status = typeof Status[keyof typeof Status];
// 結果是 "active" | "inactive" | "suspended"const enum
// 不能用 — const enum 需要在編譯時內聯值
const enum Direction {
Up = 0,
Down = 1,
}
// tsc 會把 Direction.Up 直接替換成 0
// type stripping 做不到這件事
// 替代方案:同樣用 as const
const Direction = { Up: 0, Down: 1 } as const;namespace(命名空間)
// 不能用 — namespace 需要被轉換成 IIFE
namespace Validation {
export function isEmail(s: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
}
}
// 替代方案:用 ES module
// validation.ts
export function isEmail(s: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
}舊式 Decorator(experimentalDecorators)
// 舊式 decorator 不能用 — 需要 tsc 轉換
// (指 tsconfig 中 experimentalDecorators: true 的語法)
@Controller("/users")
class UserController {
@Get("/:id")
getUser(@Param("id") id: string) { ... }
}
// 注意:TC39 Stage 3 的新式 decorator 語法
// 是 JavaScript 原生提案,不依賴 tsc 轉換
// 但目前 Node.js 24 的 V8 尚未完整支援parameter properties
// 不能用 — 需要轉換成 constructor 中的賦值
class UserService {
constructor(
private readonly db: Database, // 這是 TypeScript 獨有的簡寫語法
private readonly logger: Logger
) {}
}
// 替代方案:明確寫出賦值
class UserService {
private db: Database;
private logger: Logger;
constructor(db: Database, logger: Logger) {
this.db = db;
this.logger = logger;
}
}功能支援速查表
| TypeScript 功能 | 免建置可用 | 替代方案 |
|---|---|---|
| 型別標註 / interface / type | 可以 | — |
| 泛型 | 可以 | — |
| as 斷言 / satisfies | 可以 | — |
| 型別守衛(is) | 可以 | — |
| utility types(Pick, Omit 等) | 可以 | — |
| enum | 不行 | as const 物件 |
| const enum | 不行 | as const 物件 |
| namespace | 不行 | ES module |
| experimentalDecorators | 不行 | 等待 TC39 decorator 支援 |
| parameter properties | 不行 | 明確 constructor 賦值 |
| import type / type-only import | 可以 | — |
| paths / baseUrl(tsconfig) | 不行 | Node.js subpath imports |
適用與不適用情境
適合免建置的情境
- CLI 工具與腳本 — 資料庫遷移腳本、資料處理腳本、DevOps 自動化腳本。這類工具通常不需要 enum 或 decorator,直接
node migrate.ts就能跑。 - Cron job / 背景任務 — 排程任務、佇列消費者。型別標註提供開發時的安全感,執行時直接剝離。
- 內部 API 服務 — 用 Express、Fastify 或 Hono 寫的內部微服務。不需要打包給前端,不需要產出
.d.ts檔案。 - 原型開發與 PoC — 快速驗證想法時,少一個建置步驟就是少一個摩擦力。
- 測試 — 搭配
node --test tests/*.ts,測試檔案也可以用 TypeScript 寫,不需要 ts-jest 或 vitest。
仍然需要建置步驟的情境
- 發佈 npm 套件 — 套件消費者可能使用任何 runtime 和工具鏈。你需要提供
.js+.d.ts檔案。 - 使用 NestJS 等依賴 decorator 的框架 — NestJS 核心架構依賴
experimentalDecorators,目前無法免建置。 - 大量使用 enum 的既有專案 — 如果程式碼中有上百個 enum,遷移成
as const的成本太高,不如繼續用 tsc。 - 前端應用 — React/Vue/Angular 應用本來就需要 bundler 處理 JSX/SFC/樣式,TypeScript 只是其中一環。
- 需要 path alias 的專案 — tsconfig 的
paths設定不被 Node.js 支援。需要改用 Node.js 原生的imports欄位或繼續用 bundler。
團隊遷移清單
如果你的團隊決定開始在部分場景採用免建置開發,以下是一個務實的遷移清單:
第一步:盤點程式碼中的不相容功能
# 掃描專案中使用了哪些不相容的 TypeScript 功能
# 檢查 enum 使用
grep -rn "^[[:space:]]*enum " src/ --include="*.ts" | wc -l
# 檢查 const enum
grep -rn "const enum" src/ --include="*.ts" | wc -l
# 檢查 namespace
grep -rn "^[[:space:]]*namespace " src/ --include="*.ts" | wc -l
# 檢查 experimentalDecorators
grep -n "experimentalDecorators" tsconfig.json
# 檢查 parameter properties
grep -rn "constructor.*private\|constructor.*protected\|constructor.*readonly" src/ --include="*.ts" | wc -l
# 檢查 paths alias
grep -n '"paths"' tsconfig.json第二步:調整 tsconfig.json
// tsconfig.json — 免建置開發建議設定
{
"compilerOptions": {
// 目標設定
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
// 只做型別檢查,不輸出檔案
"noEmit": true,
// 嚴格模式(開發時由 IDE 和 CI 執行型別檢查)
"strict": true,
"skipLibCheck": true,
// 避免使用不相容的功能
"verbatimModuleSyntax": true, // 強制使用 import type
// 不要啟用這些
// "experimentalDecorators": true, // 不相容
// "emitDecoratorMetadata": true, // 不相容
// "paths": { ... }, // 不相容
}
}第三步:從低風險場景開始
建議的推進順序:
- 開發用腳本 — 把
ts-node scripts/seed.ts改成node scripts/seed.ts。 - 測試 — 用
node --test取代 ts-jest,少裝一個依賴。 - 開發模式 — 把
tsx watch src/server.ts改成node --watch src/server.ts。 - 新的內部服務 — 新專案從第一天就免建置。
- 既有 production 服務 — 最後才考慮。確認沒有不相容功能後再遷移。
CI/CD 注意事項
免建置開發改變了 CI/CD 的流程設計。以下是需要調整的地方:
型別檢查必須是獨立步驟
以前 tsc 同時做型別檢查和編譯輸出。現在你跳過了編譯,型別檢查就必須明確放進 CI pipeline:
# GitHub Actions 範例
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
- run: npm ci
# 型別檢查 — 必須獨立執行
- name: Type check
run: npx tsc --noEmit
# Lint
- name: Lint
run: npx eslint src/
# 測試 — 直接用 node 跑 .ts 測試檔
- name: Test
run: node --test tests/**/*.ts
# 不需要 build 步驟了!Docker 映像檔簡化
# 免建置的 Dockerfile — 更簡潔
FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
# 直接複製 .ts 原始碼,不需要 build 步驟
COPY src/ ./src/
# 直接執行 .ts 檔案
CMD ["node", "src/server.ts"]
# 對比傳統需要建置的 Dockerfile:
# FROM node:24-slim AS builder
# WORKDIR /app
# COPY package*.json ./
# RUN npm ci
# COPY . .
# RUN npm run build <-- 不需要了
#
# FROM node:24-slim
# WORKDIR /app
# COPY --from=builder /app/dist ./dist <-- 不需要了
# COPY --from=builder /app/node_modules ./node_modules
# CMD ["node", "dist/server.js"]注意事項
- 映像檔大小 —
.ts原始碼包含型別標註,檔案比編譯後的.js稍大。對大多數應用來說差異不大,但如果你嚴格控管映像檔大小,需要評估。 - 啟動時間 — 每次啟動都需要做 type stripping。在冷啟動敏感的 serverless 環境,這個額外開銷(通常數十毫秒)可能需要測量。如果啟動時間是關鍵指標,production 仍然建議預編譯。
- source map — type stripping 保持行號不變,所以 error stack trace 天然正確。但如果你用了
--experimental-transform-types(啟用語法轉換以支援 enum 等功能),就需要另外處理 source map。 - devDependencies 中的 tsc — 即使不用 tsc 編譯,你仍然需要
typescript套件來做 CI 中的型別檢查和 IDE 支援。不要把它從 devDependencies 移除。
實戰範例:免建置的 Express API 服務
# 初始化專案
mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D typescript @types/express @types/node
# 建立 tsconfig.json
cat << 'EOF' > tsconfig.json
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true
}
}
EOF// src/server.ts
import express, { type Request, type Response } from "express";
interface CreateUserBody {
name: string;
email: string;
}
type ApiResponse<T> = {
ok: true;
data: T;
} | {
ok: false;
error: string;
};
const app = express();
app.use(express.json());
const users: Map<string, CreateUserBody & { id: string }> = new Map();
app.post("/users", (req: Request, res: Response) => {
const body = req.body as CreateUserBody;
if (!body.name || !body.email) {
const response: ApiResponse<never> = { ok: false, error: "name and email required" };
res.status(400).json(response);
return;
}
const id = crypto.randomUUID();
const user = { id, ...body };
users.set(id, user);
const response: ApiResponse<typeof user> = { ok: true, data: user };
res.status(201).json(response);
});
app.get("/users/:id", (req: Request, res: Response) => {
const user = users.get(req.params.id);
if (!user) {
const response: ApiResponse<never> = { ok: false, error: "user not found" };
res.status(404).json(response);
return;
}
const response: ApiResponse<typeof user> = { ok: true, data: user };
res.json(response);
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});# 開發模式 — 直接跑,不需要任何建置
node --watch src/server.ts
# 型別檢查(IDE 會即時做,CI 也要做)
npx tsc --noEmit
# Production 啟動
node src/server.ts常見問答
Q: 效能有差嗎?每次啟動都要做 type stripping?
Type stripping 的開銷很小(通常在數十毫秒等級),而且 Node.js 會快取處理結果。對長時間運行的服務來說,啟動時的一次性開銷完全可以忽略。但對 serverless 的冷啟動或需要頻繁重啟的場景,建議實測確認是否在可接受範圍內。
Q: 可以和現有的 tsc 建置流程並存嗎?
完全可以。最務實的做法是:開發和測試用 node app.ts 直接跑,CI 中用 tsc --noEmit 做型別檢查,production 部署你可以選擇直接跑 .ts 或繼續用 tsc 編譯後跑 .js。兩條路可以並行。
Q: ts-node 和 tsx 還需要嗎?
如果你的程式碼不使用 enum、namespace、parameter properties 等需要轉換的功能,Node.js 24 的原生支援可以完全取代 ts-node 和 tsx。少一個依賴就是少一個可能出問題的地方。
Q: 這跟 Bun / Deno 的 TypeScript 支援有什麼不同?
Bun 和 Deno 都支援完整的 TypeScript 語法轉換(包含 enum、decorator 等)。Node.js 刻意選擇了更保守的 type stripping 路線——只處理型別標註的剝離,不做語法轉換。這個設計決策的好處是實作更簡單、更可預測,缺點是支援的 TypeScript 功能較少。
總結
Node.js 24 的原生 TypeScript 支援不是要取代 tsc,而是讓你在不需要完整編譯流程的場景中,省下建置步驟帶來的摩擦。
務實的做法是:
- 新的腳本、CLI 工具、內部服務 — 直接用
node app.ts,享受免建置的開發體驗。 - 既有專案 — 先把開發腳本和測試遷移過去,漸進式降低對 ts-node / tsx 的依賴。
- 需要 enum / decorator 的專案 — 繼續用 tsc,或者考慮逐步將 enum 改為
as const。 - CI/CD — 記得把
tsc --noEmit加進 pipeline,型別檢查不能省。
免建置開發在 2026 年已經是可行的,但「可行」的前提是你了解它的邊界。知道什麼能用、什麼不能用,才能做出正確的技術決策。