TypeScript 模組與命名空間:重新匯出(export * from)
簡介
在大型前端或 Node.js 專案中,模組化 是維持程式碼可讀性、可維護性與可重用性的關鍵。TypeScript 在 ES6 模組的基礎上提供了更完整的型別系統,使得開發者可以在編譯階段就捕捉到許多錯誤。
當專案的模組結構變得複雜時,往往會出現「集中匯出」的需求:把多個子模組的介面一次性暴露給外部使用者,而不必在每個匯入點都寫一長串 import 語句。這時 重新匯出(re‑export),尤其是 export * from,就派上用場了。
本文將深入說明 export * from 的語法、運作原理與實務應用,並提供完整範例、常見陷阱與最佳實踐,幫助你在 TypeScript 專案中更有效率地管理模組。
核心概念
1. 什麼是「重新匯出」?
在 ES6(以及 TypeScript)中,匯出(export) 與 匯入(import) 是模組之間的橋樑。
- 直接匯出:在同一檔案內宣告並匯出變數、函式、類別等。
- 重新匯出(re‑export):把其他模組已經匯出的成員,再一次匯出給更上層的使用者。
// a.ts
export const foo = 1;
export function bar() { return 'bar'; }
// b.ts
export * from './a'; // 重新匯出 a.ts 所有的公開成員
b.ts 並沒有自行定義任何成員,它的作用僅是 「轉發」 a.ts 的所有匯出,讓外部只需要 import b.ts 即可取得 foo、bar。
2. export * from 的語法規則
| 語法 | 說明 |
|---|---|
export * from "./module" |
匯出 所有 具名匯出的成員(不包括 default) |
export { name1, name2 } from "./module" |
只匯出指定的成員 |
export { default as Foo } from "./module" |
重新匯出 default 成員並重新命名 |
export * as ns from "./module" (ES2020) |
把整個模組匯出為命名空間 ns(在 TypeScript 3.8 起支援) |
注意:
export * from不會 重新匯出default成員。如果想要同時匯出default,需要額外寫一行export { default } from "./module"。
3. 為什麼要使用 export * from?
集中入口(Barrel)
把散落在多個檔案的匯出彙總到一個「桶」檔(barrel),讓使用者只需要一條import路徑即可取得全部功能。減少重複代碼
避免在每個匯入點都寫相同的import { X } from '../../utils/x',降低維護成本。提升封裝性
透過重新匯出,你可以決定哪些成員對外可見,哪些保持私有。
程式碼範例
以下示範 5 個常見的 export * from 用法,從基礎到比較進階的情境。
範例 1:最簡單的重新匯出(Barrel)
// src/models/user.ts
export interface User {
id: number;
name: string;
}
export function getUser(id: number): User {
return { id, name: `User${id}` };
}
// src/models/post.ts
export interface Post {
id: number;
title: string;
}
export function getPost(id: number): Post {
return { id, title: `Post${id}` };
}
// src/models/index.ts <-- Barrel
export * from './user';
export * from './post';
使用方式:
import { User, getUser, Post, getPost } from '@/models';
const u = getUser(1);
const p = getPost(2);
重點:外部只需要匯入一次
@/models,就能同時取得user與post相關的型別與函式。
範例 2:同時匯出 default 成員
// lib/logger.ts
export default class Logger {
log(msg: string) { console.log(msg); }
}
export function setLevel(l: string) { /* ... */ }
// lib/index.ts
export * from './logger'; // 只匯出具名成員
export { default } from './logger'; // 重新匯出 default
使用方式:
import Logger, { setLevel } from '@/lib';
new Logger().log('Hello');
setLevel('debug');
技巧:將
default重新匯出時,務必寫在 單獨 的語句,否則會被export *忽略。
範例 3:使用別名(as)重新匯出部分成員
// utils/math.ts
export const PI = 3.14159;
export function add(a: number, b: number) { return a + b; }
export function mul(a: number, b: number) { return a * b; }
// utils/index.ts
export { PI as π } from './math'; // 重新匯出並改名
export { add, mul as multiply } from './math';
使用方式:
import { π, add, multiply } from '@/utils';
console.log(π); // 3.14159
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
說明:別名讓匯出名稱更貼近使用情境,且不會與其他模組產生衝突。
範例 4:使用 export * as ns 建立命名空間(ES2020)
// services/api.ts
export function fetchUser(id: number) { /* ... */ }
export function fetchPost(id: number) { /* ... */ }
// services/index.ts
export * as API from './api';
使用方式:
import { API } from '@/services';
API.fetchUser(1);
API.fetchPost(2);
優點:一次匯出整個模組為命名空間,呼叫時更具可讀性,且避免了大量單獨匯入的繁瑣。
範例 5:多層 Barrel 結合 TypeScript 路徑別名
// src/components/button.tsx
export const Button = () => <button>Click</button>;
// src/components/input.tsx
export const Input = () => <input />;
// src/components/index.ts <-- 第一層 barrel
export * from './button';
export * from './input';
// src/index.ts <-- 第二層 barrel
export * from './components';
tsconfig.json 配置路徑別名:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
使用方式:
import { Button, Input } from '@/';
說明:透過兩層 barrel,外部只需要匯入根路徑
@/即可取得所有 UI 元件,極大簡化入口。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
export * 會忽略 default |
default 成員無法被重新匯出,導致匯入失敗 |
在同一檔案中額外寫 export { default } from './module' |
| 重名衝突(多個子模組匯出相同名稱) | 編譯錯誤 Duplicate identifier |
使用別名 as 或者只匯出需要的成員,避免 export * |
| 循環依賴(A → B → A) | 執行時得到 undefined 或 ReferenceError |
儘量避免相互依賴,或將共同程式抽出成獨立模組 |
| 過度聚合(一次匯出過多) | 產生巨大的 bundle,失去 tree‑shaking 效益 | 只匯出實際需要的 API,或在 package.json 設定 sideEffects: false |
| 使用舊版編譯目標(target < ES2015) | export * as ns 會編譯失敗 |
確保 tsconfig.json target 至少為 es2020,或改用手動命名空間 |
最佳實踐
- Barrel 應放在目錄根部:讓每個子目錄只保有一個
index.ts作為匯出入口,提升可預測性。 - 明確列出匯出清單:在大型專案中,使用
export { A, B } from './module'替代export *,可避免名稱衝突。 - 結合 TypeScript 路徑別名:設定
paths後,匯入時使用@/或~等簡短別名,減少相對路徑的錯誤。 - 保持
default匯出的可見性:若模組同時提供具名與預設匯出,記得在 barrel 中同時重新匯出兩者。 - 測試與 lint:使用 ESLint 的
import/no-duplicates、import/no-cycle等規則,及早捕捉重複或循環匯入問題。
實際應用場景
設計 UI 元件庫
每個元件放在獨立目錄,最外層的index.ts使用export * from './Button'、export * from './Modal',讓使用者只需import { Button, Modal } from 'my-ui'。多語系資源檔
各語系 JSON 皆以export const messages = {...}方式匯出,然後在i18n/index.ts用export * from './en'、export * from './zh-TW',讓語系切換只需要讀取一次匯入。Node.js 後端服務
各個業務邏輯(service)檔案分散在src/services/*,在src/services/index.ts使用export * from './user'、export * from './order',Controller 只需要import { UserService, OrderService } from '@/services'。共享型別定義
多個子模組都會使用相同的介面或型別別名,將它們集中在types/index.ts,並在每個子模組的index.ts重新匯出,確保所有模組引用同一份型別,避免不一致。
總結
export * from是 重新匯出 的核心語法,讓你可以在不改變原始模組的情況下,建立 集中入口(Barrel),提升程式碼的可讀性與維護性。- 它不會自動匯出
default成員;若需要同時匯出,必須額外寫export { default } from './module'。 - 使用別名、命名空間或多層 barrel 能夠更靈活地控制匯出的結構,避免名稱衝突與循環依賴。
- 在實務開發中,配合 TypeScript 的路徑別名、ESLint 規則以及適當的測試,能讓
export *發揮最大效益,同時保持 bundle 體積與 tree‑shaking 的效果。
掌握了 重新匯出 的技巧後,你將能以更乾淨、模組化的方式組織大型 TypeScript 專案,讓團隊協作更順暢,程式碼品質也更上一層樓。祝開發順利!