cover

專案能跑不代表能維護。這篇幫你把 ESLint 和 Typings 設好,讓「三個月後的你」不會想掐死「現在的你」。本篇是 系列第二篇(Jest 測試設定) 的前置。

先講結論

  • ESLint:裝好 + 跑一次 pnpm run lint,通常會噴兩個錯(unused import 和 any),修掉它們就是你寫好 TypeScript 的第一步
  • Typings 資料夾:把路由路徑之類的「魔術字串」抽成 enum,以後改路由不用全域搜尋替換
  • 完成後的資料夾結構長這樣,記住就好
src/
  routes/
  bin/
  app.ts
typings/
  routes/
    index.ts
    users.ts

ESLint:你以為你不需要,直到你 diff 出 200 行空白變更

你有沒有遇過這種事?PR review 的時候,diff 顯示改了 200 行,結果 180 行都是 tab 變 space 或多了一個空行。這就是沒有統一 linter 的下場。

安裝

一行搞定:

pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-config-airbnb-base eslint-config-prettier eslint-plugin-import \
  eslint-plugin-node eslint-plugin-prettier prettier @eslint/js typescript-eslint

套件很多對吧?簡單分類:@typescript-eslint/* 是讓 ESLint 看懂 TypeScript 的、prettier 系列是處理格式的、airbnb-base 是規則集。你不需要記,裝了就對了。

初始化

pnpm exec eslint --init

選項照這樣選:syntax and find problems → esm → none(framework)→ typescript → node。最後會生出 eslint.config.mjs

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
 
export default [
  { files: ["**/*.{js,mjs,cjs,ts}"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
    }
  }
];

argsIgnorePattern: "^_" 這條很實用 — Express 的 middleware 常常有用不到的參數(像 _req),加底線就不會被 ESLint 罵。

跑第一次 lint

package.json 加上:

"scripts": {
  "lint": "eslint 'src/**/*.ts'"
}

然後 pnpm run lint,你大概會看到兩個錯誤:

src/app.ts     → 'join' is defined but never used
src/bin/www.ts → Unexpected any. Specify a different type

第一個好解決,把 app.ts 裡沒用到的 import { join } from 'path' 刪掉。第二個比較有意思 — 我們在 www.ts 的 error handler 用了 any,要改成正確的型別。

any — 用 NodeJS.ErrnoException

www.tsonError 參數型別從 any 改成 NodeJS.ErrnoException

const onError = (error: NodeJS.ErrnoException) => {
  if (error.syscall !== 'listen') throw error;
  // ... 其餘不變
};

這不只是消 lint 警告而已。NodeJS.ErrnoException.code.syscall 這些屬性的型別定義,IDE 的自動完成會變好用很多。any 就像開車不繫安全帶,出事之前都覺得沒差。

Typings:把魔術字串關進 enum 裡

你的路由現在寫死了 '/''/users'。如果有一天要把 /users 改成 /api/users,你得搜尋整個 codebase。路由少還好,多了就是噩夢。

建立 typings 資料夾

mkdir -p typings/routes

typings/routes/index.ts

export enum IndexRoutePaths {
  HOME = '/',
  USERS = '/users',
}

typings/routes/users.ts

export enum UserRoutePaths {
  INDEX = '/',
}

更新路由來用 enum

// src/routes/index.ts
import { IndexRoutePaths } from '../../typings/routes/index';
 
router.get(IndexRoutePaths.HOME, (req: Request, res: Response) => {
  res.send('Welcome to the Home Page');
});
router.use(IndexRoutePaths.USERS, usersRouter);

以後要改路徑?改 enum 一個地方就好,TypeScript compiler 會幫你檢查有沒有漏改的。

更新 tsconfig.json

記得把 typings/ 加進 include,不然 TypeScript 不會去編譯它:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "rootDir": "./",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*", "tests/**/*", "typings/**/*"]
}

注意 rootDir./src 變成了 ./,因為現在 typings/src/ 是平行的。不改的話 TypeScript 會跟你說 typings/ 不在 rootDir 裡面。

到這裡的成果

pnpm run lint 應該零錯誤了。你的專案現在有了統一的 code style、有意義的型別定義,不再是一個「只是能跑」的原型。

下一篇 Jest 測試設定 會把測試環境也搭起來,寫幾個 route 的 integration test,讓你改 code 的時候有個安全網。


ESLint 就像交通規則 — 你可以不遵守,但遲早會出事。