TypeScript 模組與命名空間 – default export / named export
簡介
在大型前端或 Node.js 專案中,模組化 是維持程式碼可讀、可維護與可重用的基礎。TypeScript 繼承了 ES6 的模組語法,提供 default export 與 named export 兩種不同的匯出方式。掌握它們的差異與使用時機,能讓你在團隊協作、程式碼分割以及測試上得到更好的體驗。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者了解 default export 與 named export 在 TypeScript 中的使用方式,並提供實務上的應用情境,幫助你在日常開發中快速上手。
核心概念
1. 什麼是 export?
在 ES6(亦即 ES2015)之前,JavaScript 並沒有正式的模組系統,開發者只能靠全域變數或 IIFE(Immediately‑Invoked Function Expression)來模擬。ES6 引入 export 與 import,讓每個檔案(module)都能明確宣告 對外提供 的 API。
// utils.ts
export function add(a: number, b: number): number {
return a + b;
}
其他檔案只要 import { add } from "./utils" 即可使用。
2. default export
default export 代表唯一的預設匯出,一個模組只能有一個 default。匯入時不需要使用大括號,且可以自行命名。
// logger.ts
export default function log(message: string): void {
console.log(`[Log] ${message}`);
}
// app.ts
import logger from "./logger"; // 直接使用自訂的名稱
logger("Hello TypeScript!");
為何使用 default?
- 單一職責:模組只提供一個主要功能(例如 React 元件、服務類別)。
- 簡化匯入語法:使用者不必記得匯出的名稱,只要給予自己慣用的變數名即可。
3. named export
named export 允許在同一個檔案中匯出多個變數、函式或類別。匯入時必須使用大括號,且名稱必須與匯出時一致(除非使用 as 重新命名)。
// math.ts
export const PI = 3.1415;
export function multiply(a: number, b: number): number {
return a * b;
}
export class Calculator {
static square(x: number) {
return x * x;
}
}
// main.ts
import { PI, multiply, Calculator } from "./math";
console.log(PI); // 3.1415
console.log(multiply(2, 3)); // 6
console.log(Calculator.square(5)); // 25
為何使用 named?
- 多元匯出:一個模組可能同時提供工具函式、常數、類別等。
- 靜態分析友好:IDE 能自動提示可匯入的成員,減少拼寫錯誤。
4. 混用 default 與 named
雖然可以同時存在,但要注意匯入語法的差異。
// service.ts
export default class ApiService {
get() { /* ... */ }
}
export const API_ENDPOINT = "https://api.example.com";
export type User = { id: number; name: string };
// consumer.ts
import ApiService, { API_ENDPOINT, User } from "./service";
const api = new ApiService();
console.log(API_ENDPOINT);
5. 重新匯出(re‑export)
在大型專案中,常會建立「聚合模組」來統一匯出子模組的成員。
// index.ts
export { default as Logger } from "./logger";
export * from "./math";
使用者只需要一次 import { Logger, PI } from "./index" 即可取得所有功能。
程式碼範例
範例 1:React 元件的 default export
// Button.tsx
import React from "react";
type Props = {
label: string;
onClick: () => void;
};
export default function Button({ label, onClick }: Props) {
return <button onClick={onClick}>{label}</button>;
}
// App.tsx
import React from "react";
import Button from "./Button"; // 直接匯入 default
export default function App() {
return (
<div>
<Button label="點我" onClick={() => alert("Hello")} />
</div>
);
}
重點:React 元件通常使用 default export,因為每個檔案只會有一個主要的 UI 組件。
範例 2:工具函式的 named export
// string-utils.ts
/** 取得字串長度 */
export function length(str: string): number {
return str.length;
}
/** 反轉字串 */
export function reverse(str: string): string {
return str.split("").reverse().join("");
}
// demo.ts
import { length, reverse } from "./string-utils";
console.log(length("TypeScript")); // 10
console.log(reverse("abc")); // "cba"
技巧:如果你只需要其中一個函式,可以使用 tree‑shaking,只打包被使用的部份。
範例 3:同時使用 default 與 named
// db.ts
export default class Database {
connect() { console.log("connected"); }
}
export const DB_VERSION = "1.0.0";
export type Config = {
host: string;
port: number;
};
// index.ts
import Database, { DB_VERSION, Config } from "./db";
const db = new Database();
db.connect();
console.log(`Version: ${DB_VERSION}`);
const cfg: Config = { host: "localhost", port: 5432 };
範例 4:聚合模組(barrel)與重新匯出
// utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function sub(a: number, b: number): number {
return a - b;
}
// utils/string.ts
export function upper(s: string): string {
return s.toUpperCase();
}
export function lower(s: string): string {
return s.toLowerCase();
}
// utils/index.ts // ← barrel
export * from "./math";
export * from "./string";
// app.ts
import { add, upper } from "./utils";
console.log(add(5, 7)); // 12
console.log(upper("ts")); // "TS"
範例 5:動態匯入(lazy load)與 default export
// heavy-module.ts
export default function heavyTask() {
console.log("執行大型運算...");
}
// main.ts
async function run() {
const { default: heavyTask } = await import("./heavy-module");
heavyTask(); // 只有在需要時才載入
}
run();
實務意義:在前端 SPA 中,利用
import()搭配 default export 可實現 程式碼分割,減少首次載入體積。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議解決方式 |
|---|---|---|
| 同時使用 default 與 named,匯入時忘記大括號 | import foo from "./mod" 只能取得 default,若想同時取得 named 必須加大括號。 |
範例:import foo, { bar } from "./mod" |
| 命名衝突 | 匯入多個模組的 named 成員時,名稱相同會產生衝突。 | 使用 as 重新命名:import { foo as fooA } from "./a" |
| 過度依賴 default,失去自文件的語意 | 大量 default 會讓 IDE 難以自動補全,且不易閱讀。 | 若模組提供多功能,優先使用 named export;僅有單一主體時才使用 default。 |
未設定 esModuleInterop,導入 CommonJS 模組時出錯 |
TypeScript 需要 esModuleInterop: true 才能正確處理 module.exports = ...。 |
在 tsconfig.json 中開啟 esModuleInterop,或使用 import * as foo from "cjs-lib"。 |
| 忘記在 barrel 中重新匯出 default | export * from "./module" 不會重新匯出 default 成員。 |
必須額外寫 export { default as Xxx } from "./module"。 |
最佳實踐
- 遵循單一職責原則:每個檔案只提供一個「核心」功能時,使用
default export;功能多元時使用named export。 - 保持匯入一致性:團隊內部約定匯入風格(例如全部使用大括號),避免混雜。
- 利用 TypeScript 型別:
export type、export interface只會在編譯時出現,不會產生 runtime 負擔。 - 結合 lint 規則:使用
eslint-plugin-import或typescript-eslint來檢查未使用的匯入、重複匯入等問題。 - 避免過度的 re‑export:Barrel 便利但過度使用會讓依賴圖變得不透明,適度即可。
實際應用場景
| 場景 | 建議匯出方式 | 為什麼 |
|---|---|---|
| React 元件庫 | 每個元件使用 default export,再在 index.ts 中聚合 export { default as Button } from "./Button" |
使用者可一次性匯入 import { Button, Modal } from "my-ui",同時保留元件的單一預設匯出。 |
工具函式集合 (lodash、date-fns) |
named export 為主 |
呼叫者只需要其中幾個函式,可透過 tree‑shaking 減少 bundle 大小。 |
| 服務層 (API client) | default export 服務類別 + named export 常數、型別 |
類別作為主要入口,常數與型別供其他模組使用。 |
| 多語言資源檔 | named export 每個語系物件 |
讓使用者自行挑選語系:import { en, zhTW } from "./i18n"。 |
| 動態載入的插件系統 | 每個插件使用 default export,外層用 import() 動態載入 |
讓插件只在需要時載入,減少初始載入時間。 |
總結
- default export 適合「單一核心」的模組,如 React 元件、服務類別或大型功能的入口。匯入時可自行命名,語法簡潔。
- named export 則是「多元功能」的最佳選擇,支援同時匯出多個函式、常數、類別與型別,對 IDE 與 tree‑shaking 更友好。
- 兩者可以 混合使用,但必須注意匯入語法的差異與
export *不會重新匯出default。 - 在實務開發中,聚合模組(barrel)、動態匯入、以及 型別匯出 都是提升可維護性與效能的利器。
- 避免常見陷阱(命名衝突、忘記大括號、未開
esModuleInterop)並遵循最佳實踐,能讓你的 TypeScript 專案更乾淨、可擴充。
掌握了 default 與 named export 的使用技巧後,你就能在 模組設計、程式碼分割、以及 團隊協作 上游刃有餘,寫出結構清晰、效能卓越的 TypeScript 應用程式。祝開發順利 🚀