TypeScript 與 JavaScript 整合(JS Interop)
主題:allowJs / checkJs
簡介
在現代前端開發中,TypeScript 已經成為主流語言,它提供靜態型別、編譯期錯誤檢查以及更佳的 IDE 體驗,讓大型專案更易維護。然而,絕大多數既有的程式碼庫仍是 純 JavaScript,直接把專案全部改寫成 TypeScript 既不實際也不划算。此時,allowJs 與 checkJs 兩個 tsconfig.json 設定,就成為 JS 與 TS 混合開發 的關鍵橋樑。
allowJs:允許編譯器把.js檔案納入編譯流程,讓 TypeScript 能夠 引用 或 輸出 JavaScript 檔案。checkJs:在允許 JavaScript 的同時,對這些檔案執行 型別檢查,彷彿它們本身就是.ts檔案。
掌握這兩個選項,開發者可以 逐步遷移、混合使用,甚至在純 JavaScript 專案中逐漸加入型別安全,而不必一次性重構全部程式碼。
核心概念
1. allowJs:讓 TypeScript 接納 JavaScript
在 tsconfig.json 中加入:
{
"compilerOptions": {
"allowJs": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}
allowJs: true會把src目錄下的.js檔案視為編譯單位,最終會被 轉譯(依據target設定)並輸出到outDir。- 若同時存在同名的
.ts、.js,編譯器會優先使用.ts,避免衝突。
小技巧:在大型專案中,可以先把所有舊有的 JavaScript 放在
src/js,再逐步搬移到src/ts,利用include/exclude控制範圍。
2. checkJs:在 JavaScript 中啟動型別檢查
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true // 只檢查,不產出檔案
},
"include": ["src/**/*.js"]
}
checkJs: true會針對.js檔案執行 完整的型別檢查,包括未宣告變數、錯誤的函式呼叫、參數類型不符等。- 常與
// @ts-check註解搭配使用,讓個別檔案自行決定是否啟用檢查。
// @ts-check
function add(a, b) {
return a + b;
}
add(1, "2"); // ❌ TypeScript 會報錯:Argument of type 'string' is not assignable to parameter of type 'number'.
3. // @ts-nocheck 與 // @ts-ignore
// @ts-nocheck:整個檔案關閉型別檢查,適合暫時跳過難以改寫的舊程式。// @ts-ignore:僅對下一行忽略型別錯誤,適用於少量例外情況。
// @ts-nocheck
// 這裡的程式碼不會被 TypeScript 檢查
// @ts-ignore
someLegacyFunction(); // 忽略此行的型別錯誤
4. declare 與 JSDoc:為 JavaScript 補足型別資訊
即使開啟 checkJs,若沒有額外資訊,TypeScript 只能推斷 any。使用 JSDoc 或 declare 可以手動提供型別。
/**
* @typedef {Object} User
* @property {string} name 使用者名稱
* @property {number} age 年齡
*/
/**
* 取得使用者資料
* @param {number} id 使用者 ID
* @returns {Promise<User>}
*/
async function fetchUser(id) {
const resp = await fetch(`/api/user/${id}`);
return resp.json(); // TypeScript 會正確推斷回傳型別為 Promise<User>
}
程式碼範例
範例 1:從純 JavaScript 專案逐步加入 TypeScript
# 1. 初始化 TypeScript
npm i -D typescript
npx tsc --init
tsconfig.json(只開 allowJs,先不檢查):
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"allowJs": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
現在執行
npx tsc,所有.js會被編譯成.js(依設定的target),同時保留原本的 JavaScript 語法。
範例 2:在 JavaScript 中開啟型別檢查,並使用 JSDoc
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": true
},
"include": ["src/**/*.js"]
}
src/math.js:
// @ts-check
/**
* 計算兩數的乘積
* @param {number} a
* @param {number} b
* @returns {number}
*/
function multiply(a, b) {
return a * b;
}
multiply(2, 3); // ✅ 正確
multiply(2, "3"); // ❌ TypeScript 會提示錯誤
範例 3:混合使用 .ts 與 .js,利用 declare 共享型別
src/types.d.ts(全域宣告):
declare module "legacy-lib" {
export function legacyAdd(a: number, b: number): number;
}
src/legacy.js(舊函式庫):
// @ts-check
/** @type {any} */
const legacy = require("legacy-lib");
/**
* 包裝 legacyAdd,使其支援型別檢查
* @param {number} x
* @param {number} y
* @returns {number}
*/
function safeAdd(x, y) {
return legacy.legacyAdd(x, y);
}
src/app.ts(新程式):
import { safeAdd } from "./legacy";
const sum = safeAdd(5, 10); // ✅ 型別正確
範例 4:使用 // @ts-nocheck 暫時關閉檢查
// @ts-nocheck
// 這段程式碼暫時不受 TypeScript 檢查,避免大量錯誤噴出
function oldApi(data) {
// 內部大量使用 any、未宣告變數…
}
範例 5:利用 noEmit 僅做型別檢查,不產出檔案
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": true
},
"include": ["src/**/*.js"]
}
執行 npx tsc 後,不會產生 dist,但會在終端機顯示所有型別錯誤,適合 CI/CD 中作為 靜態分析 步驟。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記在 tsconfig.json 加入 allowJs |
編譯時找不到 .js 檔案,出現 File not found 錯誤。 |
確認 allowJs 為 true,或在 include/exclude 中正確指定路徑。 |
checkJs 導致大量 any 警告 |
開發者容易被噪音淹沒,失去信心。 | 先使用 // @ts-nocheck 包住舊檔案,逐步加入 JSDoc,或把 noImplicitAny 暫時關閉,待逐步改善。 |
混用 .js 與 .ts 時模組解析衝突 |
同名檔案產生兩個不同的模組,導致 duplicate identifier。 |
保持檔名唯一,或使用 paths/baseUrl 重新映射。 |
忘記在 JSDoc 中加上 @type |
TypeScript 只能推斷為 any,失去檢查價值。 |
為所有公開函式、物件加上完整的 JSDoc 型別描述。 |
在 checkJs 開啟時未設 strict |
某些錯誤仍被寬容(例如隱式 any)。 |
建議同時開啟 strict,或至少 noImplicitAny、strictNullChecks。 |
最佳實踐總結
- 先允許 (
allowJs) 再逐步檢查 (checkJs)——循序漸進避免一次性衝擊。 - 使用 JSDoc 為舊程式提供型別訊息,配合
@ts-check讓編譯器能讀取。 - 在 CI 中加入
noEmit && checkJs,確保每次提交都有型別安全的保證。 - 分層管理:把已遷移的檔案放在
src/ts,未遷移的仍在src/js,讓include/exclude清晰可控。 - 適度使用
// @ts-ignore只在必要且暫時的情況下,避免濫用成為「逃避型別」的捷徑。
實際應用場景
| 場景 | 為何使用 allowJs / checkJs |
|---|---|
| 大型遺留系統逐步升級 | 允許舊有 JavaScript 繼續編譯,同時在新模組中使用 TypeScript,減少一次性改寫成本。 |
| 第三方 JavaScript 套件未提供型別宣告 | 透過 checkJs 搭配 JSDoc,為套件自行加上型別,提升 IDE 補完與錯誤偵測。 |
| 前端微服務(Micro‑frontend)共用代碼 | 各子應用可能使用不同語言(TS/JS),allowJs 能讓主程式統一編譯、打包。 |
| CI/CD 靜態檢查 | 在建置流程中僅執行 npx tsc --noEmit,即使是純 JavaScript,也能捕捉潛在錯誤。 |
| 教育或培訓環境 | 讓新手先寫 JavaScript,透過 @ts-check 逐步感受型別系統的好處,降低學習門檻。 |
總結
allowJs 與 checkJs 為 TypeScript 與 JavaScript 混合開發 提供了兩條重要的橋樑:
allowJs讓編譯器接受任何.js檔案,確保舊有程式碼仍能被編譯、打包。checkJs則在此基礎上加入 靜態型別檢查,讓開發者在不改寫語法的前提下,逐步提升代碼品質。
透過 JSDoc、declare、以及 // @ts-check / // @ts-nocheck 等輔助工具,我們可以在 逐步遷移、混合模組、CI 靜態分析 等情境中,安全且有效地引入 TypeScript 的優勢。只要遵循 先允許、後檢查、分層管理 的策略,即可在不犧牲開發速度的同時,逐步建立起嚴謹的型別系統,為專案的長期維護奠定堅實基礎。
關鍵一句話:「讓 JavaScript 先跑,再讓 TypeScript 幫你把錯誤抓起來」,這正是
allowJs與checkJs在實務開發中的核心價值。祝你在混合開發的路上,越走越順!