本文 AI 產出,尚未審核

TypeScript 與 JavaScript 整合(JS Interop)

主題:requireimport 混用


簡介

在現代前端與 Node.js 生態系統中,ESM(ECMAScript Modules) 已成為官方標準,而許多舊有套件仍以 CommonJS (module.exports / require) 形式提供。
當我們在 TypeScript 專案裡同時面對兩種模組系統時,常會出現「該用 import 還是 require?」的困惑。

本篇文章聚焦 requireimport 混用」 的實務技巧,說明:

  • 為什麼會需要混用
  • TypeScript 如何透過編譯選項協助兩者共存
  • 常見的陷阱與最佳實踐

掌握這些概念後,你就能在 保有型別安全 的同時,順利整合舊有 CommonJS 套件與新式 ESM 模組,提升開發效率與程式碼可維護性。


核心概念

1. 模組系統的基本差異

特色 CommonJS (require) ES Module (import)
載入方式 同步,在執行時立即解析 靜態,在編譯階段即可解析(支援 tree‑shaking)
匯出方式 module.exports = {...}exports.xxx = ... export default ...export const ...
執行環境 Node.js(早期) 現代瀏覽器、Node.js("type":"module"

重點:在 TypeScript 中,import 會被編譯成符合目標環境的語法(requireimportdynamic import),而 require 則直接保留為 CommonJS 呼叫。

2. TypeScript 的編譯選項

選項 目的 常見設定
module 指定輸出模組格式 "commonjs""esnext""amd"
esModuleInterop 允許 import foo from "foo" 直接對應 module.exports = foo true(建議開啟)
allowSyntheticDefaultImports 允許在不支援預設匯入的模組上使用 import foo from "foo" true(常與 esModuleInterop 同時使用)
resolveJsonModule 允許 import data from "./data.json" true(若需要載入 JSON)

實務建議:在大多數新專案中,將 module 設為 "esnext",同時開啟 esModuleInterop,即可在 importrequire 之間無縫切換。

3. import = require() 語法

這是 TypeScript 為了兼容 CommonJS 而提供的 舊式混合語法,寫法如下:

import fs = require('fs');
  • 優點:在 module: "commonjs" 時,編譯後仍保留 require('fs'),不會產生多餘的 __importDefault 包裝。
  • 缺點:只能用於 modulecommonjs,且無法與 ES6 的 import 同時使用於同一檔案。

結論:在新專案中盡量避免 import = require(),改用 import + esModuleInterop,但在遺留代碼或需要保留 require 的情況下仍是可行方案。


程式碼範例

以下示範 5 種 常見的混用情境,並附上註解說明。

範例 1:基本的 require + import 混用(Node.js + CommonJS)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true
  }
}
// utils.ts
export function hello(name: string): string {
  return `Hello, ${name}!`;
}
// app.ts
import { hello } from "./utils";   // ES6 風格匯入
const lodash = require("lodash"); // CommonJS 風格載入

console.log(hello("TypeScript"));
console.log("Lodash version:", lodash.VERSION);

說明esModuleInterop 讓 TypeScript 能把 lodash(CommonJS)視為有預設匯出的模組,importrequire 可以自由混用。


範例 2:使用 import = require() 兼容舊版套件

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2019",
    "strict": true
  }
}
// legacy.ts
import express = require("express"); // 典型的舊式寫法

const app = express();
app.get("/", (req, res) => res.send("Hello from Express"));
app.listen(3000, () => console.log("Server running on 3000"));

重點:此寫法在 module: "commonjs" 時會直接產生 var express = require("express");,不會產生 __importDefault 包裝,效能略佳。


範例 3:動態匯入(import())搭配 require

// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
    "target": "ES2020",
    "esModuleInterop": true,
    "strict": true
  }
}
// dynamic.ts
// 靜態匯入
import { hello } from "./utils";

// 動態匯入 CommonJS 模組
async function loadLodash() {
  const _ = await import("lodash"); // 會被編譯成 __importStar()
  console.log("Chunk loaded, lodash version:", _.default.VERSION);
}

// 同步 require(在 Node.js 環境下仍可使用)
function loadFsSync() {
  const fs = require("fs");
  console.log("fs exists:", typeof fs.readFile === "function");
}

hello("World");
loadLodash();
loadFsSync();

技巧:在 ESM 專案中,動態匯入 可保留程式碼分割(code‑splitting)的好處;若同時需要載入 CommonJS,await import() 仍會自動包裝成 default 屬性。


範例 4:使用 allowSyntheticDefaultImports 匯入 JSON

// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
    "target": "ES2020",
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "strict": true
  }
}
// data.json
{
  "name": "TypeScript",
  "year": 2012
}
// json-demo.ts
import data from "./data.json"; // 直接當作預設匯入

console.log(`Project ${data.name} started in ${data.year}`);

說明allowSyntheticDefaultImports 讓 TypeScript 能把 JSON(本質上是 CommonJS)視為有預設匯出,與 import 完全兼容。


範例 5:在同一檔案內混用 importrequireimport = require()

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2019",
    "esModuleInterop": true,
    "strict": true
  }
}
// mixed.ts
import { hello } from "./utils";      // ES6 匯入
import path = require("path");        // 舊式混合
const moment = require("moment");    // 直接 require

export function demo() {
  console.log(hello("混用範例"));
  console.log("Current directory:", path.resolve("."));
  console.log("現在時間:", moment().format("YYYY-MM-DD HH:mm:ss"));
}

實務觀點:即使同一檔案內混用多種語法,只要 tsconfig 設定正確,編譯結果仍會是可執行的 CommonJS 程式碼。這在漸進式遷移舊專案時非常有用。


常見陷阱與最佳實踐

陷阱 可能的錯誤訊息或行為 解決方案
忘記開 esModuleInterop TS1259: Module '"lodash"' can only be default-imported using the 'esModuleInterop' flag tsconfig.json 加入 "esModuleInterop": true
使用 import = require()esModuleInterop 同時開啟 編譯時出現 error TS1192: Module '"foo"' has no default export 避免 同時使用兩種混合語法;選擇其一即可
在 ESM 專案中直接 require ReferenceError: require is not defined(瀏覽器) 改用 import() 動態匯入,或在 Node.js 中將 type: "module" 改為 commonjs
JSON 模組未設定 resolveJsonModule Cannot find module './data.json' tsconfig.json 加入 "resolveJsonModule": true
匯入預設匯出時忘記 .default(當 esModuleInterop 關閉) TypeError: _lodash.default is not a function 手動使用 `const _ = require('lodash'); const lodash = _.default

最佳實踐

  1. 首選 import:除非必須使用 require,盡量以 ES6 import 為主,保持語法一致性與 tree‑shaking 效益。
  2. 開啟 esModuleInterop:在大多數專案中,此選項能讓 CommonJS 套件像 ES 模組一樣使用預設匯入,減少繁瑣的 require().default 寫法。
  3. 漸進式遷移:對於已有大量 require 的專案,可先在新檔案使用 import,舊檔案維持 require,等全部遷移完畢再統一 module 設定。
  4. 嚴格型別檢查"strict": true 能即時捕捉因模組型別不匹配產生的錯誤,尤其是混用時最容易出現的隱藏問題。
  5. 測試編譯產出:在切換 moduleesModuleInterop 等選項後,執行 tsc --noEmit && node dist/... 確認執行結果與預期一致。

實際應用場景

場景 為何需要混用 建議的寫法
Node.js 後端使用 Express + 中間件套件 Express 為 CommonJS,部分中間件(如 cors)仍未提供 ESM 版 import express from "express";(開 esModuleInterop
import express = require("express");(保守寫法)
前端專案需要載入舊版 Chart.js(CommonJS) 需要在 React/Vite 中使用 import,但 Chart.js 只支 module.exports import Chart from "chart.js";esModuleInterop:true
CLI 工具同時操作 JSON 設定檔與 Node 原生 API JSON 需要 import,Node 原生 API(fspath)可直接 importrequire import fs from "fs";(ESM)
import config from "./config.json";resolveJsonModule:true
大型單頁應用(SPA)需要動態載入第三方分析 SDK 分割程式碼、減少首屏載入時間 const analytics = await import("analytics-sdk");(動態匯入)
舊有 monorepo 同時包含 TypeScript 與純 JavaScript 子模組 子模組仍以 module.exports 方式匯出 tsconfig 中設定 "allowJs": true,在 TypeScript 檔案中使用 import foo from "../legacy/module"esModuleInterop:true

總結

  • 模組混用是現實需求:在過渡期或多元環境中,requireimport 必須共存。
  • 編譯選項是關鍵esModuleInteropallowSyntheticDefaultImportsresolveJsonModule 等設定,讓 TypeScript 能自動處理 CommonJS 與 ESModule 的差異。
  • 盡量以 import 為主:除非必須使用同步 require(如某些 Node 原生 API),否則使用 import 能享受更好的靜態分析與最佳化。
  • 遵循最佳實踐:開啟嚴格模式、逐步遷移、在測試環境驗證編譯產出,能避免因混用產生的隱蔽錯誤。

掌握了上述概念與技巧後,你就能在 TypeScript 專案中自如切換 requireimport,同時保有型別安全與執行效能,為團隊的前端或後端開發奠定穩固基礎。祝開發順利!