本文 AI 產出,尚未審核

TypeScript

單元:Node.js + TypeScript(實務應用)

主題:匯入 fspathhttp 等模組


簡介

在 Node.js 中,fspathhttp 等核心模組是與檔案系統、路徑處理與網路通訊最常使用的工具。
將這些模組結合 TypeScript 使用,不僅能享有靜態型別的安全檢查,還能在編譯階段即捕捉常見錯誤,提升開發效率與程式碼可讀性。

本篇文章將說明在 TypeScript 專案中 正確匯入 這些核心模組的方式,並透過實作範例展示它們在真實專案裡的典型應用。適合剛接觸 Node.js + TypeScript 的初學者,也能為已有基礎的開發者提供最佳實踐參考。


核心概念

1. Node.js 核心模組的類型定義

Node.js 的核心模組本身是以 CommonJSrequire)的形式提供,但 TypeScript 官方已提供完整的型別宣告檔(@types/node),只要在專案中安裝:

npm i -D @types/node

即可在編譯時取得 fspathhttp 等模組的型別資訊,讓 IDE 能自動補全與錯誤提示。


2. ES 模組 (import) 與 CommonJS (require) 的差異

方式 語法 執行時行為 建議使用情境
ESM import * as fs from "fs" 靜態分析、Tree‑shaking 友好 新專案、希望使用 import/export 的開發者
CommonJS const fs = require("fs") 動態載入、相容舊有 Node 版本 需要與舊有 CommonJS 程式碼共存時

Tip:在 tsconfig.json 中將 module 設為 "es2020"(或更高)即可直接使用 import,Node.js 12 以上已支援 ES 模組。


3. 基本匯入範例

以下示範三種常見的匯入寫法,均可在 TypeScript 中使用:

// ES 模組寫法(推薦)
import * as fs from "fs";
import * as path from "path";
import { createServer, IncomingMessage, ServerResponse } from "http";

// CommonJS 寫法(若專案仍使用 require)
const fsCjs = require("fs");
const pathCjs = require("path");
const http = require("http");

注意:即使使用 import,Node.js 仍會以 CommonJS 方式載入核心模組,因為它們本身就是 CommonJS 實作。這不會影響型別檢查或執行結果。


4. 範例一:使用 fs 讀寫檔案

import * as fs from "fs";
import * as path from "path";

/**
 * 讀取指定路徑的文字檔,若檔案不存在則回傳空字串。
 */
async function readTextFile(relativeFile: string): Promise<string> {
  const absolutePath = path.resolve(__dirname, relativeFile);
  try {
    // 使用 Promise 版的 readFile
    const data = await fs.promises.readFile(absolutePath, "utf-8");
    return data;
  } catch (err) {
    // 型別守衛:只處理檔案不存在的情況
    if ((err as NodeJS.ErrnoException).code === "ENOENT") {
      console.warn(`檔案 ${absolutePath} 不存在`);
      return "";
    }
    // 重新拋出其他錯誤
    throw err;
  }
}

/**
 * 寫入文字到指定檔案,若目錄不存在會自動建立。
 */
async function writeTextFile(relativeFile: string, content: string): Promise<void> {
  const absolutePath = path.resolve(__dirname, relativeFile);
  await fs.promises.mkdir(path.dirname(absolutePath), { recursive: true });
  await fs.promises.writeFile(absolutePath, content, "utf-8");
}

// 範例呼叫
(async () => {
  await writeTextFile("./data/hello.txt", "Hello, TypeScript!");
  const txt = await readTextFile("./data/hello.txt");
  console.log(txt); // => Hello, TypeScript!
})();

重點說明

  • fs.promises 提供 Promise 版 API,配合 async/await 可寫出更易讀的非同步程式。
  • path.resolve(__dirname, ...) 讓路徑在不同執行環境下保持一致。
  • 使用 NodeJS.ErrnoException 進行錯誤類型守衛,讓 TypeScript 能正確推斷 err.code

5. 範例二:使用 path 處理檔案路徑

import * as path from "path";

/**
 * 取得檔案的副檔名(不含點號)。
 */
function getExtension(filePath: string): string {
  return path.extname(filePath).replace(/^\./, "");
}

/**
 * 合併多段路徑,並正規化成絕對路徑。
 */
function buildAbsolute(...segments: string[]): string {
  // __dirname 為當前模組所在目錄
  return path.resolve(__dirname, ...segments);
}

// 範例
const ext = getExtension("src/utils/helper.ts"); // => "ts"
const abs = buildAbsolute("../", "logs", "app.log");
console.log(`副檔名:${ext}, 絕對路徑:${abs}`);

重點說明

  • path.extname 會回傳「.ts」這樣的字串,使用正則直接去除前置的點號。
  • path.resolve 會自動處理 ... 與重複的分隔符,保證產生 絕對路徑

6. 範例三:使用 http 建立簡易伺服器

import { createServer, IncomingMessage, ServerResponse } from "http";
import * as fs from "fs";
import * as path from "path";

/**
 * 依照請求的 URL 回傳對應的靜態檔案,若找不到則回傳 404。
 */
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
  const url = req.url ?? "/";
  const filePath = url === "/" ? "/index.html" : url;
  const absolutePath = path.resolve(__dirname, "public", `.${filePath}`);

  try {
    const data = await fs.promises.readFile(absolutePath);
    // 根據副檔名決定 Content-Type
    const ext = path.extname(absolutePath).toLowerCase();
    const mime: Record<string, string> = {
      ".html": "text/html",
      ".js": "application/javascript",
      ".css": "text/css",
      ".json": "application/json",
      ".png": "image/png",
      ".jpg": "image/jpeg",
    };
    res.writeHead(200, { "Content-Type": mime[ext] || "application/octet-stream" });
    res.end(data);
  } catch (err) {
    res.writeHead(404, { "Content-Type": "text/plain" });
    res.end("404 Not Found");
  }
});

const PORT = 3000;
server.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));

重點說明

  • IncomingMessageServerResponse 皆已在 @types/node 中定義型別,讓參數自動獲得 IntelliSense。
  • 透過 fs.promises.readFile 直接以非同步方式讀取檔案,避免阻塞事件迴圈。
  • mime 物件示範 簡易的 Content‑Type 對映,實務上可改用 mime-types 第三方套件。

7. 範例四:混用 ES 模組與 CommonJS(進階)

有時候專案需要同時支援舊有的 CommonJS 套件,以下示範如何在 TypeScript 中 同時使用 importrequire

// tsconfig.json 必須開啟 "esModuleInterop": true
import * as http from "http";
import * as path from "path";

// 透過 require 取得只匯出單一函式的套件(例如 legacy 的 node-fetch@2)
const fetch = require("node-fetch");

// 使用 fetch 呼叫外部 API
async function getJson(url: string) {
  const res = await fetch(url);
  return await res.json();
}

// 範例
(async () => {
  const data = await getJson("https://api.github.com/repos/microsoft/TypeScript");
  console.log(`TS repo stars: ${data.stargazers_count}`);
})();

關鍵設定

{
  "compilerOptions": {
    "module": "es2020",
    "target": "es2020",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}
  • esModuleInteropimport foo from "cjs-module"require 之間的相容性更好。

常見陷阱與最佳實踐

陷阱 說明 最佳實踐
忘記安裝 @types/node TypeScript 無法取得核心模組的型別,會出現 Cannot find module 'fs' 錯誤。 npm i -D @types/node,並在 tsconfig.jsontypeRootstypes 中包含。
使用相對路徑時忘記 __dirname 在不同工作目錄下執行程式會找不到檔案。 永遠path.resolve(__dirname, ...) 產生絕對路徑。
混用 importrequire 而未開 esModuleInterop 會產生 default is not a function 或類似錯誤。 tsconfig.json 設定 esModuleInterop: true,或改用全 import 方式。
同步 I/O 造成阻塞 fs.readFileSync 在大量請求時會阻塞事件迴圈。 優先使用 fs.promisesfs.readFile 搭配 async/await
未正確處理錯誤類型 catch (err) 時直接使用 err.code 會被 TypeScript 判為 any 使用 if (err instanceof Error && 'code' in err)NodeJS.ErrnoException 進行類型守衛。

實際應用場景

  1. 日誌系統:使用 fs.promises.appendFile 寫入每日 log,搭配 path.join(__dirname, 'logs', ${date}.log')` 產生日期檔名。
  2. 靜態檔案伺服器:結合 httpfspath,快速建立開發環境的本機伺服器,支援 HTML、CSS、JS 等資源。
  3. CLI 工具:透過 process.argv 讀取指令列參數,使用 fs 讀寫設定檔(JSON),再利用 path 處理相對路徑。
  4. 簡易 Proxy:使用 http.createServer 接收請求,利用 node-fetch(或原生 http)轉發至後端 API,最後把回應寫回 res
  5. 檔案監控fs.watch 搭配 path 判斷變更的檔案類型,實作自動重新編譯或重新載入的開發工具。

總結

  • 匯入 Node.js 核心模組 時,只要安裝 @types/node,就能在 TypeScript 中得到完整的型別支援。
  • ES 模組 (import) 是推薦的寫法,配合 tsconfig.jsonmoduleesModuleInterop 設定,可兼容舊有 CommonJS 套件。
  • 使用 fs.promisespath 的工具函式,可以讓檔案與路徑操作既安全又易讀;同時配合 async/await,避免阻塞事件迴圈。
  • http 伺服器 中將檔案讀取與 MIME 判斷結合,可快速構建開發用的靜態服務。
  • 注意常見陷阱(未安裝型別、相對路徑、同步 I/O 等),遵守最佳實踐,能讓 TypeScript + Node.js 的開發體驗更順暢。

掌握了 匯入與使用 fspathhttp 的正確方式後,你就能在 TypeScript 專案裡自由地操作檔案、處理路徑以及建立網路服務,為日後的更大型應用(如 Express、NestJS)奠定堅實基礎。祝開發順利!