Node.js + TypeScript(實務應用)
主題:Node.js 型別套件(@types/node)
簡介
在使用 Node.js 開發伺服器端程式時,TypeScript 能為我們提供靜態型別、智能提示與編譯時錯誤檢查,極大提升開發效率與程式碼品質。
然而,Node.js 本身是以 JavaScript 為主,官方並未內建 TypeScript 型別定義。若想在 TypeScript 中直接使用 fs、http、process 等核心模組,就必須透過 @types/node 這個型別套件(DefinitelyTyped)來取得完整的型別資訊。
本篇文章將說明 @types/node 的安裝、主要型別結構、實用範例,以及在實務開發中常見的坑與最佳實踐,幫助初學者快速上手、讓中階開發者更深入掌握型別的威力。
核心概念
1. 為什麼需要 @types/node
| 項目 | 說明 |
|---|---|
| 型別安全 | 防止因傳入錯誤參數而在執行階段拋出例外。 |
| IDE 智慧提示 | VS Code、WebStorm 等編輯器能顯示函式簽名、屬性說明。 |
| 文件自動補全 | 直接在程式碼中看到 fs.readFile 的 overload 版本。 |
| 跨平台一致性 | 同一套型別檔案支援 Windows、Linux、macOS。 |
註:@types/node 只是一組
.d.ts宣告檔,並不會改變 Node.js 的執行行為。
2. 安裝方式
# 使用 npm
npm install --save-dev @types/node
# 若使用 yarn
yarn add -D @types/node
提示:在
tsconfig.json中的types設定若留空,編譯器會自動載入node_modules/@types內的所有套件;若想限制載入範圍,請明確列出["node"]。
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"types": ["node"] // ← 只載入 Node.js 型別
}
}
3. 常見的型別入口
| 模組 | 主要型別檔 | 常用介面/類別 |
|---|---|---|
fs |
fs.d.ts |
fs.ReadStream、fs.WriteFileOptions |
http |
http.d.ts |
IncomingMessage、ServerResponse |
path |
path.d.ts |
ParsedPath |
process |
process.d.ts |
ProcessEnv、SignalConstants |
events |
events.d.ts |
EventEmitter |
4. 程式碼範例
範例 1️⃣ 讀寫檔案(fs)
import { readFile, writeFile } from "fs/promises";
// 使用型別安全的選項物件
async function copyFile(src: string, dest: string): Promise<void> {
// 讀取檔案,返回 Buffer
const data: Buffer = await readFile(src);
// 寫入檔案,使用 { encoding: "utf8" } 讓 TypeScript 知道選項型別
await writeFile(dest, data, { encoding: "utf8" });
}
// 呼叫時,IDE 會提示 src、dest 必須是 string
copyFile("./data/input.txt", "./data/output.txt")
.then(() => console.log("Copy finished"))
.catch(console.error);
說明:
readFile的回傳型別根據傳入的encoding會自動切換為string或Buffer,@types/node為此提供 overload,讓開發者不必自行斷言。
範例 2️⃣ 建立 HTTP 伺服器(http)
import http, { IncomingMessage, ServerResponse } from "http";
const server = http.createServer(
(req: IncomingMessage, res: ServerResponse): void => {
// 取得 URL 與 method,IDE 會自動補全屬性
const { url, method } = req;
console.log(`收到 ${method} 請求:${url}`);
// 設定回應標頭與內容
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", path: url }));
}
);
server.listen(3000, () => console.log("伺服器啟動於 http://localhost:3000"));
重點:
IncomingMessage與ServerResponse的型別讓我們在操作req.headers、res.writeHead時不會寫錯屬性名稱。
範例 3️⃣ 使用 EventEmitter(events)
import { EventEmitter } from "events";
class MyTimer extends EventEmitter {
private intervalId?: NodeJS.Timeout;
start(seconds: number): void {
let count = 0;
this.intervalId = setInterval(() => {
count += 1;
this.emit("tick", count); // 發出自訂事件
if (count >= seconds) this.stop();
}, 1000);
}
stop(): void {
if (this.intervalId) clearInterval(this.intervalId);
this.emit("end");
}
}
const timer = new MyTimer();
timer.on("tick", (n: number) => console.log(`第 ${n} 秒`));
timer.on("end", () => console.log("計時結束"));
timer.start(5);
說明:
NodeJS.Timeout是setInterval、setTimeout回傳的型別,使用@types/node後可以直接在變數宣告時加上型別,避免誤用number(在瀏覽器環境中回傳值為number)。
範例 4️⃣ 讀取環境變數(process)
// 定義自訂的環境變數型別,提升安全性
interface MyEnv extends NodeJS.ProcessEnv {
NODE_ENV: "development" | "production";
PORT?: string; // 可為 undefined
API_KEY: string;
}
// 直接斷言為 MyEnv,IDE 會提示缺少的變數
const env = process.env as MyEnv;
if (!env.API_KEY) {
throw new Error("缺少 API_KEY 環境變數");
}
const port = Number(env.PORT ?? 3000);
console.log(`伺服器將於 ${port} 埠口啟動 (${env.NODE_ENV})`);
技巧:透過擴充
ProcessEnv,可以在開發階段即捕捉缺少或型別不符的環境變數。
範例 5️⃣ 路徑操作(path)
import { join, resolve, basename } from "path";
const projectRoot = resolve(__dirname, ".."); // __dirname 為 Node.js 全域變數
const entryFile = join(projectRoot, "src", "index.ts");
console.log("專案根目錄:", projectRoot);
console.log("入口檔案路徑:", entryFile);
console.log("檔名:", basename(entryFile)); // 輸出 index.ts
要點:
__dirname、__filename在 TypeScript 中已被@types/node定義為string,不會出現「未宣告」的錯誤。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 型別版本不一致 | @types/node 的版本必須與實際執行的 Node.js 版本相容(例如 Node 18 需要 @types/node@18) |
在 package.json 中使用 npm view node version 確認,或安裝 npm i -D @types/node@latest 並搭配 engines 設定 |
| 全域變數衝突 | 若同時使用瀏覽器型別(@types/web),setTimeout 等全域函式會產生重複定義 |
在 tsconfig.json 的 lib 中僅保留需要的 "es2022",或在 types 限制只載入 ["node"] |
錯誤的 esModuleInterop |
沒有啟用 esModuleInterop 時,import fs from "fs" 會出錯 |
設定 "esModuleInterop": true,或改用 import * as fs from "fs" |
忽略 Promise 版 API |
Node.js 10+ 提供 fs/promises,但舊範例仍使用 callback 版,會失去型別推斷的好處 |
優先使用 fs/promises,或自行為 callback 版加上 util.promisify |
未處理 null / undefined |
某些 API 回傳 `string | null(如 fs.readFileSync的encoding` 參數),若直接使用會產生 runtime error |
最佳實踐
- 保持型別套件同步:每次升級 Node.js 時,同步升級
@types/node,避免因 API 新增或變更而產生型別錯誤。 - 啟用
strict:在tsconfig.json中開啟strict、noImplicitAny、strictNullChecks,讓型別檢查更完整。 - 使用
--watch+ts-node:開發階段可使用ts-node-dev或nodemon -x ts-node,即時看到型別錯誤與執行結果。 - 自訂全域介面:如上範例的
MyEnv,將環境變數、全域設定寫成介面,提升可讀性與安全性。 - 模組別名:若專案中大量使用
path.join(__dirname, ...),可在tsconfig.json設定"paths",讓路徑引用更簡潔。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@root/*": ["src/*"]
}
}
}
實際應用場景
| 場景 | 需求 | 如何運用 @types/node |
|---|---|---|
| REST API 伺服器 | 讀寫檔案、處理請求、環境變數 | http、fs、process.env 型別確保路由參數與回傳資料一致 |
| CLI 工具 | 解析命令列參數、輸出彩色文字、檔案操作 | process.argv、readline、chalk(配合 @types/chalk)的型別互補 |
| 背景任務(Worker) | 計時、事件發射、資料庫連線 | EventEmitter、setInterval、NodeJS.Timer 型別避免忘記清除計時器 |
| 微服務間的檔案傳輸 | 使用 stream 以管道方式傳遞大檔案 |
fs.createReadStream、stream.Transform 的型別讓 pipeline 結構清晰 |
| 容器化部署 | 依賴 process.env、process.pid、process.on('SIGTERM') |
型別保證信號處理與退出流程不會因參數錯誤而失效 |
案例:在一個使用
Express+TypeScript的服務中,我們把process.env包裝成Config類別,所有模組皆透過Config.get('DB_HOST')取得值。由於Config內部使用了MyEnv介面,開發者在寫測試或新增環境變數時,IDE 會立刻提示缺少或型別不符的變數,減少部署失敗的風險。
總結
- @types/node 為 Node.js 核心 API 補上了完整的 TypeScript 型別,使我們在開發伺服器端程式時能享受到編譯時檢查與 IDE 智慧提示。
- 正確安裝、對應 Node 版本、在
tsconfig.json中適當設定types與strict,是避免型別衝突的關鍵。 - 透過範例可以看到,從檔案 I/O、HTTP 伺服器、事件驅動到環境變數管理,每一個核心模組都有對應的型別,只要善加利用,就能寫出更安全、可維護的程式碼。
- 常見的陷阱如版本不一致、全域變數衝突與缺乏嚴格檢查,都可以透過最佳實踐(升級同步、限制載入、啟用
strict)來避免。
在實務專案中,把型別視為合約,讓團隊成員在協作時都有一致的期待,最終能提升開發速度、降低錯誤率。希望本篇文章能幫助你快速掌握 @types/node,並在未來的 Node.js + TypeScript 專案中發揮最大的效益。祝開發順利!