本文 AI 產出,尚未審核

ExpressJS (TypeScript)

單元:基礎概念與環境設定

主題:為什麼要搭配 TypeScript


簡介

在 Node.js 生態系統中,Express 仍是最流行的 Web 框架之一。它的設計哲學是「最小、靈活」,讓開發者可以用極少的程式碼快速建立 API 或網站。
然而,隨著專案規模的擴大、團隊成員增多,純 JavaScript 的動態類型特性會帶來維護成本、錯誤追蹤困難等問題。這時 TypeScript(TS)成為提升開發效率與程式品質的最佳拍檔。

本文將說明為什麼在 Express 專案中加入 TypeScript 能夠 減少 bug、提升可讀性、加速開發,並提供實作範例與最佳實踐,讓你在建立下一個 Express 應用時,能夠毫不猶豫地選擇 TypeScript。


核心概念

1. 靜態型別(Static Typing)讓錯誤提早顯現

在純 JavaScript 中,變數的型別在執行時才會被檢查,常見的錯誤像是傳入 undefined、拼寫錯誤的屬性名稱,往往只能在跑測試或正式上線後才被發現。
TypeScript 透過編譯階段的型別檢查,能夠在 編寫程式時即捕捉 這類問題。

// src/types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

// src/routes/user.ts
import { Request, Response } from 'express';
import { User } from '../types/user';

export const getUser = (req: Request, res: Response) => {
  // ✅ 編譯器會提醒 id 可能為 undefined
  const id = Number(req.params.id);
  // 假設從資料庫取得使用者
  const user: User = { id, name: 'Alice', email: 'alice@example.com' };
  res.json(user);
};

重點:若 req.params.id 為非數字,Number() 仍會回傳 NaN,但 TypeScript 已確保 user 物件符合 User 介面,使得後續的屬性存取不會因型別不符而出錯。


2. 智慧提示(IntelliSense)提升開發速度

使用 VS Code 等支援 TypeScript 的編輯器時,自動補完參數說明跳轉到定義 等功能都會變得更精準。對於剛接觸 Express API(如 req.bodyres.status)的開發者,這些即時提示能大幅降低學習曲線。

// src/middleware/logger.ts
import { Request, Response, NextFunction } from 'express';

export const logger = (req: Request, _res: Response, next: NextFunction) => {
  // 這裡會自動顯示 Request 介面的所有屬性
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
};

3. 結構化的專案型別(Project‑wide Types)

在大型專案裡,共用型別(如資料庫模型、DTO、錯誤回傳格式)往往散落於多個檔案。使用 TypeScript 可以把這些型別集中管理,確保前後端或不同模組之間的契約一致。

// src/types/api-response.ts
export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

// src/controllers/auth.ts
import { Request, Response } from 'express';
import { ApiResponse } from '../types/api-response';

export const login = (req: Request, res: Response) => {
  // 假設驗證成功,回傳 token
  const token = 'jwt-token';
  const result: ApiResponse<{ token: string }> = {
    success: true,
    data: { token },
  };
  res.json(result);
};

4. 編譯時的程式碼轉換(Transpilation)

TypeScript 會在編譯階段把 ES6+ 語法(如 async/awaitimport/export)轉成目標環境可執行的 JavaScript。這意味著 即使你的執行環境僅支援 ES5,也能安心使用最新語法,保持程式碼的可讀性與一致性。

// src/app.ts
import express from 'express';
import { logger } from './middleware/logger';
import { login } from './controllers/auth';

const app = express();

app.use(express.json());
app.use(logger);

app.post('/login', login);

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

:上述檔案會被 tsc(TypeScript Compiler)編譯成 dist/app.js,在 package.json 中設定 "start": "node dist/app.js" 即可執行。


5. 型別守衛(Type Guards)與窄化(Narrowing)

在處理外部輸入(如 query string、JSON body)時,常需要先驗證資料型別再使用。TypeScript 提供 型別守衛,讓開發者寫出更安全的程式碼。

// src/utils/validation.ts
export function isNumber(value: any): value is number {
  return typeof value === 'number' && !isNaN(value);
}

// src/routes/product.ts
import { Request, Response } from 'express';
import { isNumber } from '../utils/validation';

export const getProduct = (req: Request, res: Response) => {
  const id = Number(req.params.id);
  if (!isNumber(id)) {
    return res.status(400).json({ success: false, error: 'Invalid product id' });
  }
  // 此時 TypeScript 已確定 id 為 number
  // ... 取得商品資料
  res.json({ success: true, data: { id, name: 'Sample Product' } });
};

程式碼範例(實作練習)

以下提供 5 個常見的 Express + TypeScript 範例,每個範例皆附有說明與註解,供讀者直接貼上測試。

範例 1:建立基本的 Express 伺服器

// src/server.ts
import express, { Request, Response } from 'express';

const app = express();
const PORT = process.env.PORT ?? 3000;

// 內建的 JSON 解析中介軟體
app.use(express.json());

app.get('/', (req: Request, res: Response) => {
  res.send('Hello, Express + TypeScript!');
});

app.listen(PORT, () => {
  console.log(`🚀 Server listening on http://localhost:${PORT}`);
});

說明:使用 express.json()req.body 自動轉成物件;RequestResponse 的型別讓 IDE 能正確提示。


範例 2:使用自訂型別的路由參數

// src/routes/user.ts
import { Router, Request, Response } from 'express';
import { User } from '../types/user';

const router = Router();

// 假資料
const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
];

// 取得單一使用者
router.get('/:id', (req: Request<{ id: string }>, res: Response) => {
  const id = Number(req.params.id);
  const user = users.find(u => u.id === id);
  if (!user) {
    return res.status(404).json({ success: false, error: 'User not found' });
  }
  res.json({ success: true, data: user });
});

export default router;

重點Request<{ id: string }>req.params.id 的型別固定為 string,避免意外寫成 req.params.ID


範例 3:全域錯誤處理中介軟體(Error‑handling Middleware)

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { ApiResponse } from '../types/api-response';

export const errorHandler = (
  err: Error,
  _req: Request,
  res: Response,
  // 必須保留 next 參數才能被辨識為錯誤中介軟體
  _next: NextFunction
) => {
  console.error(err);
  const result: ApiResponse<null> = {
    success: false,
    error: err.message,
  };
  res.status(500).json(result);
};

使用方式:在 app.ts 最後 app.use(errorHandler);,所有未捕獲的例外都會走這條路徑。


範例 4:結合 Joi 進行 DTO 驗證(Data Transfer Object)

// src/dto/createUser.dto.ts
import Joi from 'joi';

export const createUserSchema = Joi.object({
  name: Joi.string().min(2).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).required(),
});

export type CreateUserDto = {
  name: string;
  email: string;
  password: string;
};
// src/routes/auth.ts
import { Router, Request, Response, NextFunction } from 'express';
import { createUserSchema, CreateUserDto } from '../dto/createUser.dto';
import { User } from '../types/user';

const router = Router();

router.post('/register', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const value: CreateUserDto = await createUserSchema.validateAsync(req.body);
    // 假設寫入資料庫
    const newUser: User = { id: Date.now(), ...value };
    res.status(201).json({ success: true, data: newUser });
  } catch (err) {
    next(err); // 交給全域 errorHandler 處理
  }
});

export default router;

技巧:透過 validateAsync 直接得到符合 CreateUserDto 型別的物件,避免在後續程式碼中再次做型別斷言。


範例 5:使用 express-async-errors 處理非同步錯誤

// src/app.ts
import 'express-async-errors'; // 只要匯入一次即可
import express from 'express';
import userRouter from './routes/user';
import authRouter from './routes/auth';
import { errorHandler } from './middleware/errorHandler';

const app = express();
app.use(express.json());

app.use('/users', userRouter);
app.use('/auth', authRouter);

// 必須放在所有路由最後
app.use(errorHandler);

export default app;
// src/routes/user.ts(加入 async/await 範例)
router.get('/:id', async (req, res) => {
  const id = Number(req.params.id);
  // 假設從資料庫非同步取得
  const user = await getUserFromDb(id); // 若拋錯,會自動被 errorHandler 捕捉
  if (!user) {
    return res.status(404).json({ success: false, error: 'User not found' });
  }
  res.json({ success: true, data: user });
});

說明express-async-errors 讓我們不必在每個 async 路由手動 try/catch,錯誤會自動傳遞給全域錯誤處理器,程式碼更乾淨。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記安裝型別定義 (@types/express) TypeScript 只能辨識原生 JavaScript,缺少型別會失去 IntelliSense。 npm i -D @types/express @types/node
any 濫用 把所有變數都寫成 any,等於回到 JavaScript,失去型別保護。 盡量使用具體介面或型別別名,必要時才使用 unknown 再做型別守衛。
型別不一致的環境變數 process.env 回傳 `string undefined`,直接使用會產生編譯錯誤。
路由參數型別寫錯 req.params.id 被寫成 number,編譯不會錯,執行時會得到 undefined 使用 Request<{ id: string }> 明確宣告參數型別,再在程式內轉型。
未設定 tsconfig.jsonmoduleResolution 造成 import 路徑找不到,編譯失敗。 tsconfig.json 加入 "moduleResolution": "node""esModuleInterop": true

最佳實踐

  1. 分層管理型別:將資料庫模型、DTO、API 回傳格式分別放在 src/typessrc/dtosrc/interfaces 中,保持單一職責。
  2. 嚴格模式:在 tsconfig.json 開啟 "strict": true,讓編譯器檢查所有可能的隱式 anynullundefined
  3. 使用 ESLint + Prettier:配合 @typescript-eslint 插件,統一程式碼風格並自動檢查型別相關的潛在問題。
  4. 單元測試結合型別:使用 jest + ts-jest,讓測試失敗時同時顯示型別錯誤。
  5. 持續整合(CI):在 GitHub Actions 或 GitLab CI 中加入 npm run lint && npm run build,確保每次提交都通過型別檢查。

實際應用場景

場景 為什麼需要 TypeScript
大型電商平台(多個微服務、複雜資料模型) 型別能保證跨服務的資料契約一致,減少因欄位變更造成的斷層。
多人協作的後端 API(前端與後端同時開發) 前端開發者可直接引用後端的型別(如 UserProduct),避免手動撰寫重複介面。
需要自動生成 OpenAPI / Swagger 使用 tsoarouting-controllers 等套件,透過 TypeScript 裝飾器直接產生 API 文件。
高頻率部署的 SaaS 服務 嚴格的型別檢查讓 CI/CD 流程更可靠,降低因程式碼變更導致的服務中斷。
與第三方 SDK 整合(如 Stripe、AWS SDK) 大多數官方 SDK 已提供 TypeScript 型別,直接使用可減少手動轉型與錯誤。

案例:某金融科技公司將原本的 Express + JavaScript 專案遷移至 TypeScript,僅在 3 個月內完成,Bug 數下降 45%,同時前端團隊可直接使用 @types 產出的介面,開發速度提升約 30%


總結

TypeScriptExpress 結合,不只是趨勢,更是提升開發效率、降低維護成本的實務選擇。

  • 靜態型別讓錯誤 在編譯階段 就被捕捉,避免跑到生產環境才顯現。
  • 完整的型別系統提供 智慧提示,新手上手更快,團隊協作更順暢。
  • 透過 DTO、型別守衛、全域錯誤處理,我們可以寫出結構清晰、可測試的程式碼。
  • 配合 ts-node-devexpress-async-errorseslint 等工具,開發體驗與生產品質皆能同步提升。

在未來的課程中,我們將一步步帶你 從零建置 TypeScript + Express 專案,深入探討路由、驗證、測試與部署,讓你能在實務專案中自信地使用 TypeScript,打造 安全、可維護且高效能 的 Web API。祝你寫程式快樂,型別永遠保駕護航! 🚀