TypeScript
單元:Node.js + TypeScript(實務應用)
主題:匯入 fs、path、http 等模組
簡介
在 Node.js 中,fs、path、http 等核心模組是與檔案系統、路徑處理與網路通訊最常使用的工具。
將這些模組結合 TypeScript 使用,不僅能享有靜態型別的安全檢查,還能在編譯階段即捕捉常見錯誤,提升開發效率與程式碼可讀性。
本篇文章將說明在 TypeScript 專案中 正確匯入 這些核心模組的方式,並透過實作範例展示它們在真實專案裡的典型應用。適合剛接觸 Node.js + TypeScript 的初學者,也能為已有基礎的開發者提供最佳實踐參考。
核心概念
1. Node.js 核心模組的類型定義
Node.js 的核心模組本身是以 CommonJS(require)的形式提供,但 TypeScript 官方已提供完整的型別宣告檔(@types/node),只要在專案中安裝:
npm i -D @types/node
即可在編譯時取得 fs、path、http 等模組的型別資訊,讓 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}`));
重點說明
IncomingMessage與ServerResponse皆已在@types/node中定義型別,讓參數自動獲得 IntelliSense。- 透過
fs.promises.readFile直接以非同步方式讀取檔案,避免阻塞事件迴圈。 mime物件示範 簡易的 Content‑Type 對映,實務上可改用mime-types第三方套件。
7. 範例四:混用 ES 模組與 CommonJS(進階)
有時候專案需要同時支援舊有的 CommonJS 套件,以下示範如何在 TypeScript 中 同時使用 import 與 require:
// 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
}
}
esModuleInterop讓import foo from "cjs-module"與require之間的相容性更好。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
忘記安裝 @types/node |
TypeScript 無法取得核心模組的型別,會出現 Cannot find module 'fs' 錯誤。 |
npm i -D @types/node,並在 tsconfig.json 的 typeRoots 或 types 中包含。 |
使用相對路徑時忘記 __dirname |
在不同工作目錄下執行程式會找不到檔案。 | 永遠 以 path.resolve(__dirname, ...) 產生絕對路徑。 |
混用 import 與 require 而未開 esModuleInterop |
會產生 default is not a function 或類似錯誤。 |
在 tsconfig.json 設定 esModuleInterop: true,或改用全 import 方式。 |
| 同步 I/O 造成阻塞 | fs.readFileSync 在大量請求時會阻塞事件迴圈。 |
優先使用 fs.promises 或 fs.readFile 搭配 async/await。 |
| 未正確處理錯誤類型 | catch (err) 時直接使用 err.code 會被 TypeScript 判為 any。 |
使用 if (err instanceof Error && 'code' in err) 或 NodeJS.ErrnoException 進行類型守衛。 |
實際應用場景
- 日誌系統:使用
fs.promises.appendFile寫入每日 log,搭配path.join(__dirname, 'logs',${date}.log')` 產生日期檔名。 - 靜態檔案伺服器:結合
http、fs、path,快速建立開發環境的本機伺服器,支援 HTML、CSS、JS 等資源。 - CLI 工具:透過
process.argv讀取指令列參數,使用fs讀寫設定檔(JSON),再利用path處理相對路徑。 - 簡易 Proxy:使用
http.createServer接收請求,利用node-fetch(或原生http)轉發至後端 API,最後把回應寫回res。 - 檔案監控:
fs.watch搭配path判斷變更的檔案類型,實作自動重新編譯或重新載入的開發工具。
總結
- 匯入 Node.js 核心模組 時,只要安裝
@types/node,就能在 TypeScript 中得到完整的型別支援。 - ES 模組 (
import) 是推薦的寫法,配合tsconfig.json的module、esModuleInterop設定,可兼容舊有 CommonJS 套件。 - 使用
fs.promises、path的工具函式,可以讓檔案與路徑操作既安全又易讀;同時配合async/await,避免阻塞事件迴圈。 - 在
http伺服器 中將檔案讀取與 MIME 判斷結合,可快速構建開發用的靜態服務。 - 注意常見陷阱(未安裝型別、相對路徑、同步 I/O 等),遵守最佳實踐,能讓 TypeScript + Node.js 的開發體驗更順暢。
掌握了 匯入與使用 fs、path、http 的正確方式後,你就能在 TypeScript 專案裡自由地操作檔案、處理路徑以及建立網路服務,為日後的更大型應用(如 Express、NestJS)奠定堅實基礎。祝開發順利!