本文 AI 產出,尚未審核

TypeScript 與 JavaScript 整合(JS Interop)

主題:混合專案(.js + .ts)


簡介

在現代前端開發中,TypeScript 以靜態型別、編譯期錯誤檢查等優勢,逐漸成為大型專案的首選語言。然而,許多既有的程式碼庫仍然是純 JavaScript,或是團隊在逐步遷移時不想一次全部改寫。這時 混合專案(同時包含 .js.ts 檔案)就成了最實用的過渡方案。

透過正確的設定與技巧,我們可以在同一個專案裡自由呼叫 JavaScript 函式、使用第三方 JS 套件,同時享受 TypeScript 帶來的型別安全與開發體驗。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者完整掌握「.js + .ts」混合專案的核心要點,讓你在真實專案中順利推動 TypeScript 化。


核心概念

1. TypeScript 專案的基本設定

在混合專案中,tsconfig.json 必須告訴編譯器要處理哪些檔案、如何解析 JavaScript。以下是一個常見的設定範例:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "allowJs": true,          // 允許編譯 .js 檔案
    "checkJs": false,         // 若想要檢查 .js,設為 true
    "noEmit": false,
    "outDir": "./dist",
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],    // 包含 .ts 與 .js
  "exclude": ["node_modules"]
}
  • allowJs:允許 TypeScript 編譯器處理 .js 檔案,使兩種語言可以共存。
  • checkJs:若開啟,編譯器會對 JavaScript 檔案執行型別檢查(需搭配 // @ts-check 註解)。
  • esModuleInterop:讓 CommonJS 模組可以以 import 方式使用,減少相容性問題。

小技巧:在遷移過程中,先把 checkJs 關掉,等到 JavaScript 部分逐步補上型別註解後,再開啟以獲得更完整的檢查。


2. 引入 JavaScript 模組的方式

2.1 ES Module (import)

如果 JavaScript 檔案本身使用 export,可以直接以 ES Module 方式匯入:

// src/util.js
export function add(a, b) {
  return a + b;
}
// src/main.ts
import { add } from "./util.js";   // 必須加 .js 擴展名 (Node/Esm)
const sum = add(3, 5);
console.log(sum); // 8

注意:在 TypeScript 中匯入 .js 檔案時,必須保留副檔名.js),否則編譯器會找不到對應檔案。

2.2 CommonJS (require)

對於舊有的 module.exports/require 風格,esModuleInterop 讓我們可以這樣寫:

// src/legacy.js
module.exports = {
  multiply: (x, y) => x * y,
};
// src/main.ts
import legacy from "./legacy.js";   // 透過 default import 取得整個模組
const product = legacy.multiply(4, 6);
console.log(product); // 24

如果不想使用 esModuleInterop,也可以直接使用 require

// src/main.ts
const legacy = require("./legacy.js");
const product = legacy.multiply(4, 6);

3. 為 JavaScript 加上型別宣告

純 JavaScript 本身沒有型別資訊,為了在 TypeScript 中取得自動完成與錯誤檢查,我們可以:

3.1 使用 JSDoc

.js 檔案內加入 JSDoc 註解,編譯器會根據註解推斷型別:

/**
 * 計算兩個數字的平均值
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
export function average(a, b) {
  return (a + b) / 2;
}

3.2 宣告檔 (.d.ts)

若不想改動原始 JavaScript,亦可在 src/types 目錄下建立對應的宣告檔:

// src/types/legacy.d.ts
declare module "./legacy.js" {
  export function multiply(x: number, y: number): number;
}

這樣在 main.ts 中使用 legacy.multiply 時,就會得到正確的型別提示。


4. 編譯與執行流程

步驟 說明
1️⃣ tsc tsconfig.json.ts.js 皆編譯至 dist/.js 會直接拷貝,.ts 會轉譯)
2️⃣ node dist/main.js 執行編譯後的入口檔案,所有模組路徑已指向 dist/ 中的檔案
3️⃣ (Optional) ts-node 在開發階段直接執行 TypeScript 檔案,ts-node 會自動處理混合檔案

建議:在 CI/CD 流程中使用 npm run build && node dist/main.js,確保所有 JavaScript 都已正確搬移。


程式碼範例

以下示範一個 混合專案 的完整結構與核心功能。

目錄結構

my-mixed-project/
├─ src/
│  ├─ util.js          // 純 JavaScript 函式庫
│  ├─ legacy.js        // CommonJS 模組
│  ├─ math.ts          // TypeScript 模組
│  └─ index.ts         // 入口檔案
├─ tsconfig.json
└─ package.json

1️⃣ util.js(ES Module + JSDoc)

// src/util.js
/**
 * 將字串轉為大寫
 * @param {string} str
 * @returns {string}
 */
export function shout(str) {
  return str.toUpperCase() + "!";
}

/**
 * 判斷是否為偶數
 * @param {number} n
 * @returns {boolean}
 */
export function isEven(n) {
  return n % 2 === 0;
}

2️⃣ legacy.js(CommonJS)

// src/legacy.js
module.exports = {
  /** 取得隨機整數 */
  randomInt: (max) => Math.floor(Math.random() * max),
};

3️⃣ math.ts(TypeScript)

// src/math.ts
export const PI = 3.14159;

/**
 * 計算圓形面積
 * @param radius 半徑
 */
export function circleArea(radius: number): number {
  return PI * radius * radius;
}

4️⃣ index.ts(混合使用)

// src/index.ts
import { shout, isEven } from "./util.js";
import legacy from "./legacy.js";          // 透過 esModuleInterop
import { circleArea } from "./math";

// 直接使用 JavaScript 函式
console.log(shout("hello world")); // HELLO WORLD!

// 使用 CommonJS 模組
const rand = legacy.randomInt(100);
console.log(`Random number < 100: ${rand}`);

// 結合 TypeScript 與 JavaScript
if (isEven(rand)) {
  console.log(`偶數 ${rand} 的圓形面積是 ${circleArea(rand)}`);
} else {
  console.log(`奇數 ${rand},不計算面積`);
}

5️⃣ tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "allowJs": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

6️⃣ package.json(簡易腳本)

{
  "name": "my-mixed-project",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node src/index.ts"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "ts-node": "^10.9.0"
  }
}

執行 npm run build && npm start,即可看到混合專案的完整運作。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記在 import 時加 .js 副檔名 ES Module 在 Node/Esm 中必須保留副檔名,否則會跑不出錯誤。 始終寫成 import … from "./file.js"
allowJs 開啟卻忘記 checkJs 會導致 JavaScript 部分完全沒有型別檢查,錯誤被忽略。 在過渡期先使用 // @ts-check 單檔啟用檢查,或逐步開啟 checkJs
CommonJS 與 ES Module 混用產生的 default/named 匯入衝突 例如 module.exports = fnexport default fn 行為不同。 使用 esModuleInterop,或手動寫 import * as mod from "./cjs.js"
宣告檔與實際實作不一致 .d.ts 中型別與 JS 實作不匹配,會產生錯誤提示。 保持宣告檔與程式碼同步,使用 npm run linttsc --noEmit 檢查。
編譯輸出路徑錯誤導致找不到模組 outDirrootDir 配置不當,執行時找不到 .js 確認 tsconfig.jsonoutDirrootDirpackage.json 中的 type 設定一致。

最佳實踐

  1. 逐檔遷移:先從最關鍵的模組開始加上 .ts,其餘保持 .js,減少一次性改動的風險。
  2. 使用 JSDoc:在不想改檔名的情況下,給予 JavaScript 函式完整的型別註解,提升 IDE 補全與錯誤偵測。
  3. 建立統一的型別宣告目錄:例如 src/types/,集中管理所有第三方或 legacy 模組的 .d.ts,便於維護。
  4. CI 中加入 tsc --noEmit:確保每次提交的程式碼都能通過型別檢查,即使是純 JavaScript。
  5. 避免在同一檔案內混用 export defaultmodule.exports:保持模組風格一致,降低相容性問題。

實際應用場景

場景 為何適合混合專案 範例說明
大型遺留系統 完全改寫成本高,且業務邏輯穩定。 先在新功能模組使用 .ts,舊的計算引擎保留 .js,透過 declare module 讓新程式碼安全呼叫。
逐步導入 TypeScript 團隊成員對 TypeScript 熟悉度不同。 先在 UI 組件使用 .tsx,而服務層仍以 .js 為主,隨時可把服務層搬到 .ts
混合第三方套件 某些套件只提供 JavaScript 檔案且缺少型別宣告。 npm i --save-dev @types/xxxx(若有)或自行寫 *.d.ts,讓 TypeScript 能正確辨識。
Node.js + ESM 與 CJS 共存 有些工具只能以 CommonJS 方式匯入(例如 webpack 插件)。 透過 esModuleInterop 同時支援 import … fromrequire()
前端微前端架構 各子應用可能使用不同語言或編譯設定。 主殼程式以 TypeScript 撰寫,子應用保留原本的 JavaScript,透過全域事件或共享模組進行互動。

總結

混合專案(.js + .ts)提供了一條 低風險、可漸進 的 TypeScript 轉型路徑。透過正確的 tsconfig 設定、合理的模組匯入方式、以及適當的型別補強(JSDoc、.d.ts),開發者可以在不犧牲既有功能的前提下,逐步享受到 TypeScript 帶來的 型別安全、開發工具支援與可維護性提升

在實務上,記得:

  • 保留 .js 副檔名開啟 allowJs,讓編譯器能同時處理兩種檔案。
  • 使用 JSDoc 或宣告檔 為 JavaScript 補上型別資訊,提升 IDE 體驗。
  • 遵守最佳實踐(逐檔遷移、CI 型別檢查、統一型別目錄),避免常見的相容性陷阱。

只要掌握上述概念與技巧,無論是遺留系統還是新創專案,都能在 混合專案 的框架下,平滑且有效率地推動 TypeScript 化,為團隊與產品的長期健康奠定堅實基礎。祝開發順利,玩得開心!