TypeScript 與 JavaScript 整合(JS Interop)
主題:require 與 import 混用
簡介
在現代前端與 Node.js 生態系統中,ESM(ECMAScript Modules) 已成為官方標準,而許多舊有套件仍以 CommonJS (module.exports / require) 形式提供。
當我們在 TypeScript 專案裡同時面對兩種模組系統時,常會出現「該用 import 還是 require?」的困惑。
本篇文章聚焦 「require 與 import 混用」 的實務技巧,說明:
- 為什麼會需要混用
- 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會被編譯成符合目標環境的語法(require、import或dynamic 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,即可在import與require之間無縫切換。
3. import = require() 語法
這是 TypeScript 為了兼容 CommonJS 而提供的 舊式混合語法,寫法如下:
import fs = require('fs');
- 優點:在
module: "commonjs"時,編譯後仍保留require('fs'),不會產生多餘的__importDefault包裝。 - 缺點:只能用於
module為commonjs,且無法與 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)視為有預設匯出的模組,import與require可以自由混用。
範例 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:在同一檔案內混用 import、require、import = 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 |
最佳實踐
- 首選
import:除非必須使用require,盡量以 ES6import為主,保持語法一致性與 tree‑shaking 效益。 - 開啟
esModuleInterop:在大多數專案中,此選項能讓 CommonJS 套件像 ES 模組一樣使用預設匯入,減少繁瑣的require().default寫法。 - 漸進式遷移:對於已有大量
require的專案,可先在新檔案使用import,舊檔案維持require,等全部遷移完畢再統一module設定。 - 嚴格型別檢查:
"strict": true能即時捕捉因模組型別不匹配產生的錯誤,尤其是混用時最容易出現的隱藏問題。 - 測試編譯產出:在切換
module、esModuleInterop等選項後,執行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(fs、path)可直接 import 或 require |
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) |
總結
- 模組混用是現實需求:在過渡期或多元環境中,
require與import必須共存。 - 編譯選項是關鍵:
esModuleInterop、allowSyntheticDefaultImports、resolveJsonModule等設定,讓 TypeScript 能自動處理 CommonJS 與 ESModule 的差異。 - 盡量以
import為主:除非必須使用同步require(如某些 Node 原生 API),否則使用import能享受更好的靜態分析與最佳化。 - 遵循最佳實踐:開啟嚴格模式、逐步遷移、在測試環境驗證編譯產出,能避免因混用產生的隱蔽錯誤。
掌握了上述概念與技巧後,你就能在 TypeScript 專案中自如切換 require 與 import,同時保有型別安全與執行效能,為團隊的前端或後端開發奠定穩固基礎。祝開發順利!