本文 AI 產出,尚未審核

ExpressJS (TypeScript) – Static Files 與 Template Engines

主題:TypeScript 中的模板引擎型別支援


簡介

在使用 Express 建立 Web 應用時,模板引擎 (Template Engine) 是將資料渲染成 HTML 的關鍵工具。
當我們以 TypeScript 撰寫 Express 專案時,除了要正確設定模板引擎本身,還必須讓 編譯器知道模板檔案的型別,才能享受到自動完成、型別檢查與錯誤提示等好處。

如果缺少型別支援,開發者往往會在執行階段才發現變數名稱寫錯、傳遞的參數不符合模板需求,造成除錯成本大幅提升。本文將說明在 TypeScript 專案中,如何為常見的 Express 模板引擎(如 Pug、EJS、Handlebars)提供完整的型別支援,並示範實作步驟與最佳實踐,讓你在開發過程中 寫得更安全、寫得更快


核心概念

1. 為什麼需要「模板檔案的型別」

  • 型別安全:在 res.render('view', data) 時,編譯器可以檢查 data 是否符合模板所期待的屬性。
  • IDE 補完:VSCode、WebStorm 等編輯器會根據型別資訊提供變數與屬性的自動完成。
  • 維護友好:大型專案常有多個開發者共同維護,明確的型別宣告能減少溝通成本。

2. 基本的 TypeScript 設定

tsconfig.json 中,確保以下兩項設定已啟用:

{
  "compilerOptions": {
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,      // 防止第三方型別套件衝突
    "strict": true,
    "baseUrl": "./src",
    "paths": {
      "*": ["node_modules/*", "src/types/*"]
    }
  }
}
  • skipLibCheck 可以避免因第三方套件的型別檔案過舊而產生編譯錯誤。
  • paths 讓我們可以把自訂型別檔放在 src/types 目錄下,讓 TypeScript 自動找尋。

3. 為不同模板引擎建立型別宣告

3.1 Pug

Pug(原 Jade)在 TypeScript 社群中已有官方型別套件 @types/pug,但仍需要告訴 Express 渲染時的 locals 型別。

// src/types/pug.d.ts
import 'express';

declare global {
  // 這裡定義所有 Pug 模板共用的 locals
  interface PugLocals {
    title: string;
    user?: {
      name: string;
      email: string;
    };
    // 依需求自行擴充
  }

  // 為 Express 的 render 方法加上泛型
  namespace Express {
    interface Response {
      render(view: string, options?: PugLocals): void;
    }
  }
}

重點:使用 declare global 把型別掛到全域,讓所有 res.render 都能自動取得 PugLocals

3.2 EJS

EJS 的型別支援較少,我們可以自行建立簡易的型別檔:

// src/types/ejs.d.ts
import 'express';

declare global {
  interface EJSTemplateData {
    pageTitle: string;
    items: string[];
    // 其他變數自行加入
  }

  namespace Express {
    interface Response {
      render(view: string, options?: EJSTemplateData): void;
    }
  }
}

技巧:若專案有多個視圖使用不同的 locals,建議以 泛型 包裝 render,如下所示:

declare global {
  namespace Express {
    interface Response {
      render<T = any>(view: string, options?: T): void;
    }
  }
}

這樣在每次呼叫 res.render 時,可以直接指定型別:

res.render<EJSTemplateData>('list.ejs', { pageTitle: '清單', items: ['A', 'B'] });

3.3 Handlebars (hbs)

Handlebars 官方有 @types/express-handlebars,但仍需要為 locals 提供型別:

// src/types/hbs.d.ts
import 'express';

declare global {
  interface HbsLocals {
    layout?: string;
    title: string;
    // 任意自訂屬性
    [key: string]: any;
  }

  namespace Express {
    interface Response {
      render(view: string, options?: HbsLocals): void;
    }
  }
}

4. 讓 TypeScript 能夠讀取 .pug、.ejs、*.hbs 檔案

若你在程式碼中直接 import 模板(例如使用 require),需要告訴編譯器這些檔案的模組型別:

// src/types/templates.d.ts
declare module '*.pug' {
  const compiled: (locals?: any) => string;
  export default compiled;
}
declare module '*.ejs' {
  const compiled: (data?: any) => string;
  export default compiled;
}
declare module '*.hbs' {
  const compiled: (context?: any) => string;
  export default compiled;
}

程式碼範例

以下示範一個 完整的 Express + TypeScript + Pug 專案,涵蓋型別宣告、路由、錯誤處理與靜態檔案服務。

範例 1:專案結構

my-express-ts/
│
├─ src/
│   ├─ routes/
│   │   └─ index.ts
│   ├─ views/
│   │   ├─ layout.pug
│   │   └─ index.pug
│   ├─ public/
│   │   └─ css/
│   │       └─ style.css
│   ├─ types/
│   │   ├─ pug.d.ts
│   │   └─ templates.d.ts
│   └─ app.ts
│
├─ tsconfig.json
└─ package.json

範例 2:app.ts – 基本設定與型別匯入

// src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import path from 'path';

// 先把自訂型別檔載入(只要檔名以 .d.ts 結尾,TS 會自動讀取)
import './types/pug';
import './types/templates';

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

// 1️⃣ 設定 view engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

// 2️⃣ 靜態檔案服務
app.use(express.static(path.join(__dirname, 'public')));

// 3️⃣ 內建路由
import indexRouter from './routes/index';
app.use('/', indexRouter);

// 4️⃣ 基本錯誤處理
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  console.error(err);
  res.status(500).send('Server Error');
});

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

範例 3:路由檔 index.ts – 使用型別安全的 res.render

// src/routes/index.ts
import { Router, Request, Response, NextFunction } from 'express';

const router = Router();

router.get('/', (req: Request, res: Response, next: NextFunction) => {
  // 直接使用已在 pug.d.ts 中定義的 PugLocals
  const locals = {
    title: '歡迎使用 Express + TypeScript',
    user: {
      name: 'Alice',
      email: 'alice@example.com',
    },
  };

  // ✅ TypeScript 會檢查 locals 是否符合 PugLocals
  res.render('index', locals);
});

export default router;

範例 4:index.pug – 參考型別的模板

//- src/views/index.pug
extends layout

block content
  h1= title
  if user
    p 您好,#{user.name}(#{user.email})
  else
    p 您尚未登入

範例 5:切換到 EJS 時的簡易改寫

// src/app.ts (改為 EJS)
app.set('view engine', 'ejs');
// src/routes/index.ts (使用 EJS 型別)
import { Router, Request, Response, NextFunction } from 'express';
import { EJSTemplateData } from '../types/ejs';

const router = Router();

router.get('/', (req: Request, res: Response<EJSTemplateData>, next: NextFunction) => {
  const data = {
    pageTitle: 'EJS 範例',
    items: ['蘋果', '香蕉', '櫻桃'],
  };
  // 這裡會自動檢查 data 是否符合 EJSTemplateData
  res.render('list.ejs', data);
});

範例 6:自訂 Handlebars Layout

// src/app.ts
import exphbs from 'express-handlebars';

app.engine('hbs', exphbs({
  extname: '.hbs',
  defaultLayout: 'main',
  layoutsDir: path.join(__dirname, 'views/layouts')
}));
app.set('view engine', 'hbs');
{{!-- src/views/layouts/main.hbs --}}
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <title>{{title}}</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  {{{body}}}
</body>
</html>

常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記載入自訂型別檔 TypeScript 仍會把 res.render 視為 any,失去型別檢查。 確保 import './types/*.d.ts' 或把檔案放在 tsconfig.jsontypeRoots 中。
模板檔案路徑錯誤 res.render('foo') 找不到 foo.pug,會拋出 runtime error。 使用 app.set('views', ...) 絕對路徑,並在 tsconfig 中設定 baseUrl
模板 locals 與型別不一致 多傳或少傳屬性,編譯器不會警告(若未使用泛型)。 為每個視圖使用 泛型 res.render<YourLocals>('view', data),或在 pug.d.ts 中列舉所有可能的屬性。
第三方型別套件版本不匹配 @types/pugpug 主版本差距過大,會出現型別衝突。 使用 npm i -D @types/pug@latest,或在 tsconfig.json 設定 skipLibCheck: true
靜態檔案路徑被 Express 解析成視圖路徑 express.static 放在 app.use 的順序不當,會導致 /css/style.css 被當成模板渲染。 設定 express.static 設定路由與 view engine

最佳實踐

  1. 分層管理型別:把所有模板相關的型別放在 src/types,並以 *.d.ts 命名,保持專案結構清晰。
  2. 使用泛型:在大型專案中,單一全局 PugLocals 可能過於寬鬆,使用 res.render<SpecificLocals>() 能更精確。
  3. 保持型別同步:每次新增或變更模板變數時,同步更新對應的介面(Interface),避免「跑掉」的 UI。
  4. 結合 ESLint + @typescript-eslint:設定 @typescript-eslint/explicit-module-boundary-types,強迫每個路由檔明確標註返回型別。
  5. 測試渲染結果:使用 supertest 搭配 cheerio 撰寫端到端測試,確保模板渲染的 HTML 結構符合預期。

實際應用場景

1. 企業內部儀表板

一個需要根據使用者權限顯示不同資料的儀表板,往往會在模板中使用大量條件判斷。透過 型別化的 locals,可以在開發階段就捕捉到遺漏的屬性或錯誤的資料型別,減少因 UI 錯誤導致的業務問題。

2. 多語系網站

多語系網站會把文字字典傳入模板,例如 { t: (key: string) => string }。若字典介面缺少某些關鍵字,編譯器會直接提醒開發者,避免在瀏覽器看到 undefined 的文字。

3. 電子商務平台的商品頁

商品頁面需要渲染商品名稱、價格、庫存、促銷資訊等多個欄位。使用 EJS + 泛型,可以在服務端保證每筆商品資料都有完整的欄位,前端模板不會因缺少欄位而發生渲染錯誤。

4. 靜態文件與模板混用

在同一個 Express 專案裡,同時提供 SPA 前端的靜態資源(如 React build)與 伺服器端渲染的 Handlebars 頁面。透過正確設定 express.static 的優先順序與型別化的 res.render,即可確保兩者互不干擾,且開發者在寫路由時能清楚分辨哪個回傳 HTML、哪個回傳 JSON。


總結

  • 在 TypeScript 中使用 Express 模板引擎時,型別支援是提升開發效率與程式品質的關鍵
  • 透過 自訂全域型別宣告declare global)以及 泛型 res.render<T>,可以讓編譯器檢查傳入模板的 locals,避免 runtime 錯誤。
  • 為每種常見的模板引擎(Pug、EJS、Handlebars)建立對應的 .d.ts 檔案,並在 tsconfig.json 中正確設定 typeRootspaths,即可讓 TypeScript 正確辨識 .pug/.ejs/.hbs 檔案。
  • 實務上,在企業儀表板、多語系站點或電商平台等需要高度可靠 UI 的情境下,型別化的模板渲染能顯著降低除錯成本。
  • 最後,別忘了配合 ESLint、單元測試與 CI,將型別檢查納入開發流程,才能真正做到「寫得安全、寫得快」的開發體驗。

祝你在 Express + TypeScript 的旅程中,寫出 型別安全、可維護 的 Web 應用! 🚀