本文 AI 產出,尚未審核

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 即可取得 foobar

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

  1. 集中入口(Barrel)
    把散落在多個檔案的匯出彙總到一個「桶」檔(barrel),讓使用者只需要一條 import 路徑即可取得全部功能。

  2. 減少重複代碼
    避免在每個匯入點都寫相同的 import { X } from '../../utils/x',降低維護成本。

  3. 提升封裝性
    透過重新匯出,你可以決定哪些成員對外可見,哪些保持私有。


程式碼範例

以下示範 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,就能同時取得 userpost 相關的型別與函式。


範例 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) 執行時得到 undefinedReferenceError 儘量避免相互依賴,或將共同程式抽出成獨立模組
過度聚合(一次匯出過多) 產生巨大的 bundle,失去 tree‑shaking 效益 只匯出實際需要的 API,或在 package.json 設定 sideEffects: false
使用舊版編譯目標(target < ES2015) export * as ns 會編譯失敗 確保 tsconfig.json target 至少為 es2020,或改用手動命名空間

最佳實踐

  1. Barrel 應放在目錄根部:讓每個子目錄只保有一個 index.ts 作為匯出入口,提升可預測性。
  2. 明確列出匯出清單:在大型專案中,使用 export { A, B } from './module' 替代 export *,可避免名稱衝突。
  3. 結合 TypeScript 路徑別名:設定 paths 後,匯入時使用 @/~ 等簡短別名,減少相對路徑的錯誤。
  4. 保持 default 匯出的可見性:若模組同時提供具名與預設匯出,記得在 barrel 中同時重新匯出兩者。
  5. 測試與 lint:使用 ESLint 的 import/no-duplicatesimport/no-cycle 等規則,及早捕捉重複或循環匯入問題。

實際應用場景

  1. 設計 UI 元件庫
    每個元件放在獨立目錄,最外層的 index.ts 使用 export * from './Button'export * from './Modal',讓使用者只需 import { Button, Modal } from 'my-ui'

  2. 多語系資源檔
    各語系 JSON 皆以 export const messages = {...} 方式匯出,然後在 i18n/index.tsexport * from './en'export * from './zh-TW',讓語系切換只需要讀取一次匯入。

  3. Node.js 後端服務
    各個業務邏輯(service)檔案分散在 src/services/*,在 src/services/index.ts 使用 export * from './user'export * from './order',Controller 只需要 import { UserService, OrderService } from '@/services'

  4. 共享型別定義
    多個子模組都會使用相同的介面或型別別名,將它們集中在 types/index.ts,並在每個子模組的 index.ts 重新匯出,確保所有模組引用同一份型別,避免不一致。


總結

  • export * from重新匯出 的核心語法,讓你可以在不改變原始模組的情況下,建立 集中入口(Barrel),提升程式碼的可讀性與維護性。
  • 它不會自動匯出 default 成員;若需要同時匯出,必須額外寫 export { default } from './module'
  • 使用別名、命名空間或多層 barrel 能夠更靈活地控制匯出的結構,避免名稱衝突與循環依賴。
  • 在實務開發中,配合 TypeScript 的路徑別名、ESLint 規則以及適當的測試,能讓 export * 發揮最大效益,同時保持 bundle 體積與 tree‑shaking 的效果。

掌握了 重新匯出 的技巧後,你將能以更乾淨、模組化的方式組織大型 TypeScript 專案,讓團隊協作更順暢,程式碼品質也更上一層樓。祝開發順利!