本文 AI 產出,尚未審核

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.ReadStreamfs.WriteFileOptions
http http.d.ts IncomingMessageServerResponse
path path.d.ts ParsedPath
process process.d.ts ProcessEnvSignalConstants
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 會自動切換為 stringBuffer@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"));

重點IncomingMessageServerResponse 的型別讓我們在操作 req.headersres.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.TimeoutsetIntervalsetTimeout 回傳的型別,使用 @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.jsonlib 中僅保留需要的 "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.readFileSyncencoding` 參數),若直接使用會產生 runtime error

最佳實踐

  1. 保持型別套件同步:每次升級 Node.js 時,同步升級 @types/node,避免因 API 新增或變更而產生型別錯誤。
  2. 啟用 strict:在 tsconfig.json 中開啟 strictnoImplicitAnystrictNullChecks,讓型別檢查更完整。
  3. 使用 --watch + ts-node:開發階段可使用 ts-node-devnodemon -x ts-node,即時看到型別錯誤與執行結果。
  4. 自訂全域介面:如上範例的 MyEnv,將環境變數、全域設定寫成介面,提升可讀性與安全性。
  5. 模組別名:若專案中大量使用 path.join(__dirname, ...),可在 tsconfig.json 設定 "paths",讓路徑引用更簡潔。
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@root/*": ["src/*"]
    }
  }
}

實際應用場景

場景 需求 如何運用 @types/node
REST API 伺服器 讀寫檔案、處理請求、環境變數 httpfsprocess.env 型別確保路由參數與回傳資料一致
CLI 工具 解析命令列參數、輸出彩色文字、檔案操作 process.argvreadlinechalk(配合 @types/chalk)的型別互補
背景任務(Worker) 計時、事件發射、資料庫連線 EventEmittersetIntervalNodeJS.Timer 型別避免忘記清除計時器
微服務間的檔案傳輸 使用 stream 以管道方式傳遞大檔案 fs.createReadStreamstream.Transform 的型別讓 pipeline 結構清晰
容器化部署 依賴 process.envprocess.pidprocess.on('SIGTERM') 型別保證信號處理與退出流程不會因參數錯誤而失效

案例:在一個使用 Express + TypeScript 的服務中,我們把 process.env 包裝成 Config 類別,所有模組皆透過 Config.get('DB_HOST') 取得值。由於 Config 內部使用了 MyEnv 介面,開發者在寫測試或新增環境變數時,IDE 會立刻提示缺少或型別不符的變數,減少部署失敗的風險。


總結

  • @types/node 為 Node.js 核心 API 補上了完整的 TypeScript 型別,使我們在開發伺服器端程式時能享受到編譯時檢查與 IDE 智慧提示。
  • 正確安裝、對應 Node 版本、在 tsconfig.json 中適當設定 typesstrict,是避免型別衝突的關鍵。
  • 透過範例可以看到,從檔案 I/O、HTTP 伺服器、事件驅動到環境變數管理,每一個核心模組都有對應的型別,只要善加利用,就能寫出更安全、可維護的程式碼。
  • 常見的陷阱如版本不一致、全域變數衝突與缺乏嚴格檢查,都可以透過最佳實踐(升級同步、限制載入、啟用 strict)來避免。

在實務專案中,把型別視為合約,讓團隊成員在協作時都有一致的期待,最終能提升開發速度、降低錯誤率。希望本篇文章能幫助你快速掌握 @types/node,並在未來的 Node.js + TypeScript 專案中發揮最大的效益。祝開發順利!