跳至主要內容

TypeScript 專案配置最佳實踐:從 tsconfig 到建置工具

6 分鐘閱讀 1,100 字

TypeScript 專案配置最佳實踐:從 tsconfig 到建置工具

一個良好的 TypeScript 專案配置能讓你在開發時獲得最強的型別保護,在建置時輸出乾淨的 JavaScript,並讓整個團隊保持一致的程式碼品質。但 tsconfig.json 的選項多達數十個,很多開發者對各選項的含義一知半解。本文整理出最重要的配置項目和對應的最佳實踐。

基礎 tsconfig.json 解析

{
  "compilerOptions": {
    // ===== 目標環境 =====
    "target": "ES2022",
    // 輸出的 JavaScript 版本。現代 Node.js(18+)和瀏覽器支援 ES2022
    // 不要設太低,否則編譯器需要生成大量 polyfill 程式碼

    "module": "NodeNext",
    // 模組系統。Node.js 專案用 NodeNext,瀏覽器/Vite 專案用 ESNext

    "moduleResolution": "NodeNext",
    // 模組解析策略,需與 module 一致

    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    // 引入的型別庫。瀏覽器專案需要 DOM,Node.js 專案不需要

    // ===== 嚴格模式(全部開啟!)=====
    "strict": true,
    // 開啟所有嚴格型別檢查,等同於開啟下面這些:
    // strictNullChecks, strictFunctionTypes, strictBindCallApply
    // strictPropertyInitialization, noImplicitAny, noImplicitThis, ...

    "noUncheckedIndexedAccess": true,
    // array[0] 的型別會是 T | undefined,強迫你處理 undefined
    // 這個 strict 沒有包含,但強烈建議開啟

    "exactOptionalPropertyTypes": true,
    // { foo?: string } 中,foo 只能是 string 或不存在,不能是 undefined

    // ===== 輸出設定 =====
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,          // 生成 .d.ts 型別宣告檔
    "declarationMap": true,       // 生成 .d.ts.map,讓 IDE 能跳轉到原始碼
    "sourceMap": true,            // 生成 source map,方便除錯

    // ===== 品質控制 =====
    "noUnusedLocals": true,       // 未使用的區域變數報錯
    "noUnusedParameters": true,   // 未使用的函式參數報錯
    "noImplicitReturns": true,    // 函式所有分支必須有 return
    "noFallthroughCasesInSwitch": true, // switch case 必須有 break

    // ===== 其他重要設定 =====
    "esModuleInterop": true,      // 允許 import React from 'react'(而非 import * as React)
    "forceConsistentCasingInFileNames": true, // 檔名大小寫一致(避免 Linux/Mac 差異)
    "skipLibCheck": true          // 跳過 node_modules 中的型別檢查(加速建置)
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

多環境配置:extends 繼承

大型專案通常有多個目標環境,使用 extends 避免重複:

// tsconfig.base.json(共用基礎設定)
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true
  }
}
// tsconfig.json(開發用,啟用 source map)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "sourceMap": true,
    "declaration": true
  },
  "include": ["src"]
}
// tsconfig.test.json(測試用)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": ["vitest/globals", "node"]
  },
  "include": ["src", "tests"]
}

路徑別名(Path Aliases)

告別 ../../../utils/helper,使用簡潔的路徑:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"]
    }
  }
}

但注意:TypeScript 的 paths 只是型別層面的別名,執行時需要對應工具支援

// Vite(vite.config.ts)
import { defineConfig } from 'vite';
import { resolve } from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
});

// Node.js(tsconfig-paths 或 tsc-alias)
// tsconfig-paths 在執行時解析
npx ts-node -r tsconfig-paths/register src/index.ts

// tsc-alias 在編譯後替換路徑
npx tsc && npx tsc-alias

嚴格模式的實際影響

// 開啟 strictNullChecks 後
function getUserName(id: string): string {
  const user = users.find(u => u.id === id);
  // ❌ 錯誤:user 可能是 undefined
  return user.name;

  // ✅ 正確寫法
  if (!user) throw new Error(`User ${id} not found`);
  return user.name;
}

// noUncheckedIndexedAccess 的影響
const arr = [1, 2, 3];
const first = arr[0];
// first 的型別是 number | undefined,需要檢查

const safeFirst = arr[0] ?? 0; // 使用 nullish coalescing

型別宣告管理

// package.json
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",   // 指定型別宣告檔的位置
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

建置工具整合

使用 tsx 在開發時執行 TypeScript

npm install --save-dev tsx
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

使用 tsup 建置函式庫

npm install --save-dev tsup
// tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],     // 同時輸出 CommonJS 和 ESModule
  dts: true,                 // 生成 .d.ts
  sourcemap: true,
  clean: true,               // 建置前清除 dist/
  splitting: false,          // 單一入口不需要 code splitting
  treeshake: true
});

ESLint 型別感知規則

// eslint.config.js
import tseslint from 'typescript-eslint';

export default tseslint.config(
  tseslint.configs.strictTypeChecked,
  tseslint.configs.stylisticTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname
      }
    },
    rules: {
      // 禁止 @ts-ignore(用 @ts-expect-error 替代)
      '@typescript-eslint/ban-ts-comment': ['error', {
        'ts-ignore': 'allow-with-description'
      }],

      // 強制使用型別導入
      '@typescript-eslint/consistent-type-imports': ['error', {
        prefer: 'type-imports'
      }],

      // 避免浮動的 Promise(未 await 的 async 呼叫)
      '@typescript-eslint/no-floating-promises': 'error'
    }
  }
);

常見配置錯誤

  1. skipLibCheck: false:在 CI 中非常慢,除非你在維護函式庫,否則應設為 true

  2. "module": "CommonJS" + "moduleResolution": "NodeNext":不相容的組合

  3. paths 別名沒有對應建置工具設定:型別檢查通過但執行時報錯

  4. strict: false 節省時間?:長期看反而增加 bug 率,一開始就開嚴格模式更划算

推薦的配置基礎

可以使用社群維護的 tsconfig 基礎包:

npm install --save-dev @tsconfig/strictest
{
  "extends": "@tsconfig/strictest",
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "outDir": "./dist"
  }
}

小結

好的 TypeScript 配置是一次性投資,能為整個專案生命週期帶來收益。核心原則是:開啟最嚴格的型別檢查,讓型別錯誤在編譯時就被發現,而不是等到執行時崩潰。搭配 ESLint 的型別感知規則,TypeScript 就能發揮出最大的防禦性程式設計能力。

分享這篇文章