TypeScript Learning

列舉 (Enums) 與模組 (Modules) 簡介

⏱️預計 235 分鐘
📖TypeScript 課程
🎯實戰導向

第 10 課:列舉 (Enums) 與模組 (Modules) 簡介

學習目標

  • 理解列舉 (Enum) 嘅作用,並掌握數字列舉同字串列舉嘅定義同使用方法
  • 認識常數列舉及其優點
  • 了解列舉嘅常見使用場景
  • 初步理解 TypeScript (及 ES6) 中模組嘅基本概念
  • 學識如何使用 export 匯出模組成員以及使用 import 匯入模組成員
  • 明白使用模組嘅好處,例如程式碼組織同重用

1. 列舉 (Enums)

列舉 (Enum) 係 TypeScript 新增嘅特性 (JavaScript 原生冇),佢允許我哋為一組相關嘅常數值 (通常係數字或字串) 定義一個友好嘅名稱集合。使用列舉可以提高程式碼嘅可讀性同可維護性,避免使用魔法數字 (magic numbers) 或硬編碼字串。

1.1 數字列舉 (Numeric Enums)

數字列舉嘅成員會被賦予數字值。預設情況下,第一個成員嘅值係 0,之後嘅成員會自動遞增 1。你亦可以手動為成員賦值。

範例:

typescript
1// 預設值
2enum Direction {
3  Up,    // 0
4  Down,  // 1
5  Left,  // 2
6  Right  // 3
7}
8let move: Direction = Direction.Up;
9console.log(move); // 輸出: 0
10console.log(Direction.Right); // 輸出: 3
11
12// 手動設定初始值
13enum ResponseStatus {
14  Success = 200,
15  NotFound = 404,
16  Error = 500 // 之後嘅值如果冇手動設定,會從上一個值遞增
17}
18enum OrderStatus {
19  Pending, // 0
20  Processing, // 1
21  Shipped = 5, // 手動設定
22  Delivered, // 6 (從 5 遞增)
23  Cancelled // 7
24}
25console.log(ResponseStatus.Success); // 輸出: 200
26console.log(OrderStatus.Delivered); // 輸出: 6
27
28// 反向對應 (Reverse Mapping)
29// 數字列舉嘅一個特性係佢哋同時支持從列舉成員名稱獲取值,以及從值獲取成員名稱 (反向對應)。
30console.log(Direction[0]); // 輸出: "Up"
31console.log(Direction[Direction.Up]); // 輸出: "Up" (因為 Direction.Up 嘅值係 0)
32console.log(OrderStatus[6]); // 輸出: "Delivered"

程式碼解釋: 上述範例展示咗數字列舉嘅幾種情況:

  1. Direction 列舉展示咗預設行為,成員由 0 開始自動遞增。
  2. ResponseStatus 列舉展示咗如何為所有成員手動賦予特定數值。
  3. OrderStatus 列舉混合咗自動遞增同手動賦值。Shipped 被設為 5,之後嘅 Delivered 就會係 6。
  4. 反向對應係數字列舉獨有嘅特性,你可以用成員名稱取值 (例如 Direction.Up 得到 0),亦可以用值取回成員名稱 (例如 Direction[0] 得到 "Up")。

🎯 互動練習 1:創建遊戲狀態列舉

嘗試創建一個遊戲狀態列舉,並使用反向對應功能:

1.2 字串列舉 (String Enums)

字串列舉嘅每個成員都必須使用字串字面量或另一個字串列舉成員進行常數初始化。字串列舉冇反向對應嘅特性。

範例:

typescript
1enum LogLevel {
2  Debug = "DEBUG",
3  Info = "INFO",
4  Warning = "WARNING",
5  Error = "ERROR"
6  // None // 如果冇明確賦值,會報錯:Enum member must have initializer.
7}
8
9let currentLogLevel: LogLevel = LogLevel.Info;
10console.log(currentLogLevel); // 輸出: "INFO"
11// console.log(LogLevel["INFO"]); // 呢句會報錯,因為字串列舉冇反向對應 (除非你手動創建一個類似嘅結構)
12
13function logMessage(message: string, level: LogLevel): void {
14  console.log(`[${level}] ${message}`);
15}
16logMessage("User logged in", LogLevel.Info); // 輸出: [INFO] User logged in
17logMessage("Critical error occurred", LogLevel.Error); // 輸出: [ERROR] Critical error occurred

程式碼解釋:

  1. LogLevel 列舉展示咗字串列舉嘅定義,每個成員都明確賦予一個字串值。
  2. 與數字列舉唔同,字串列舉嘅成員值唔會自動遞增,而且必須初始化。
  3. 字串列舉冇反向對應。你唔可以直接用字串值去反查成員名稱 (例如 LogLevel["INFO"] 係唔得嘅)。
  4. logMessage 函式展示咗如何將字串列舉作為函式參數類型,令程式碼更清晰。

🎯 互動練習 2:創建主題系統

創建一個網站主題系統,使用字串列舉來管理不同主題:

1.3 常數列舉 (Const Enums)

常數列舉使用 const enum 關鍵字定義。佢哋喺編譯後會被完全移除,所有對列舉成員嘅引用都會被替換為其實際值 (呢個過程叫做內聯 Inlining)。咁樣可以減少編譯後嘅 JavaScript 程式碼體積,並可能帶嚟輕微嘅性能提升。常數列舉嘅成員只能係常數表達式,並且同字串列舉一樣,冇反向對應。

範例:

typescript
1const enum FileAccess {
2  None, // 預設值 0
3  Read    = 1 << 1, // 2 (位元運算, 0b010)
4  Write   = 1 << 2, // 4 (位元運算, 0b100)
5  ReadWrite = Read | Write, // 6 (位元運算, 0b110)
6  // G = "123".length // 錯誤:In 'const' enum declarations member initializer must be constant expression.
7                       // .length 唔係常數表達式
8}
9
10let accessMode = FileAccess.ReadWrite;
11console.log(accessMode);
12// 編譯後嘅 JavaScript 會類似:
13// let accessMode = 6; // FileAccess.ReadWrite 被內聯為 6
14// console.log(accessMode); // 輸出: 6
15
16// console.log(FileAccess[0]); // 錯誤:A const enum member can only be accessed using a string literal.
17                            // 而且因為被內聯,執行時期 FileAccess 物件根本唔存在,所以呢句點都唔得。

程式碼解釋:

  1. FileAccess 係一個常數列舉,用 const enum 定義。
  2. 成員嘅值可以係簡單數字,或者像範例中用位元運算符計算出嚟嘅常數值。
  3. 重要嘅係,const enum 嘅成員初始化必須係常數表達式。例如, "123".length 唔被視為常數表達式,所以會報錯。
  4. 編譯之後,所有對 FileAccess.ReadWrite 嘅引用都會直接被替換成 6FileAccess 呢個列舉本身唔會生成任何 JavaScript 物件。
  5. 因為常數列舉喺執行時期唔存在,所以你唔可以像數字列舉咁樣做反向對應,例如 FileAccess[0] 係唔允許嘅。

🎯 互動練習 3:權限管理系統

使用常數列舉創建一個權限管理系統:

1.4 列舉嘅使用場景

列舉喺好多情況下都非常有用,可以令你嘅程式碼更清晰、更安全:

  • 表示狀態: 例如訂單狀態 (OrderStatus.Pending, OrderStatus.Processing, OrderStatus.Shipped, OrderStatus.Delivered),文章狀態 (PostStatus.Draft, PostStatus.Published, PostStatus.Archived)。
  • 定義選項集合: 例如日誌級別 (LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error),用戶角色 (UserRole.Admin, UserRole.Editor, UserRole.Viewer),方向 (Direction.Up, Direction.Down)。
  • 替代魔法數字/字串: 與其喺程式碼中直接寫 if (status === 0) 或者 if (role === "admin"),不如用 if (status === OrderStatus.Pending) 或者 if (role === UserRole.Admin),可讀性大大提高,而且修改嘅時候只需要改列舉定義。
  • 與函式參數結合: 限制函式參數只能接受列舉中定義嘅值,例如 function setLogLevel(level: LogLevel) {...},咁樣調用者就唔會傳入無效嘅級別。

🎯 互動練習 4:訂單管理系統

創建一個完整嘅訂單管理系統,展示列舉嘅實際應用:

2. 模組 (Modules) - 簡介

隨著應用程式規模嘅增長,將所有程式碼放喺一個或少數幾個全域檔案中會變得難以管理同維護。模組允許我哋將程式碼分割成獨立、可重用嘅單元。每個模組都有自己嘅作用域,唔會污染全域作用域,從而避免命名衝突。TypeScript 採用 ES6 (ECMAScript 2015) 嘅模組標準。

2.1 乜嘢係模組?

喺 TypeScript (同現代 JavaScript) 中,任何包含頂層 importexport 宣告嘅檔案都被視為一個模組。相反,如果一個檔案冇頂層 importexport 宣告,佢嘅內容就會被視為全域嘅 (呢種做法通常唔推薦,因為容易引起命名衝突同埋難以管理依賴)。模組之間可以透過匯出 (export) 同匯入 (import) 機制共享功能、變數、類別等。

2.2 匯出 (Exporting - export)

你可以匯出變數 (const, let, var)、函式、類別、介面、類型別名 (type) 等。

2.2.1 命名匯出 (Named Exports)

一個模組可以有多個命名匯出。匯出時,成員需要有自己嘅名稱。

範例 (mathUtils.ts):
typescript
1// mathUtils.ts
2export const PI = 3.14159;
3
4export function add(x: number, y: number): number {
5  return x + y;
6}
7
8export function subtract(x: number, y: number): number {
9  return x - y;
10}
11
12export class Calculator {
13  multiply(a: number, b: number): number {
14    return a * b;
15  }
16  divide(a: number, b: number): number {
17    if (b === 0) {
18      throw new Error("Cannot divide by zero");
19    }
20    return a / b;
21  }
22}
23
24export interface MathOperation {
25  (x: number, y: number): number;
26}
27
28export type Point2D = { x: number; y: number };

程式碼解釋:mathUtils.ts 呢個檔案裡面:

  • PI 係一個命名匯出嘅常數。
  • addsubtract 係命名匯出嘅函式。
  • Calculator 係一個命名匯出嘅類別。
  • MathOperation 係一個命名匯出嘅介面。
  • Point2D 係一個命名匯出嘅類型別名。 所有這些都可以被其他模組匯入使用。

2.2.2 預設匯出 (Default Exports - export default)

每個模組最多只能有一個預設匯出。預設匯出通常用於匯出模組嘅主要功能或最常用嘅部分。當匯入預設匯出嘅成員時,你可以為佢指定任何名稱。

範例 (greeter.ts):
typescript
1// greeter.ts
2// 方法一:直接匯出函式定義
3export default function greet(name: string): string {
4  return `Hello, ${name}!`;
5}
6
7// 方法二:先定義,再預設匯出
8// function greetSomeone(name: string): string {
9//   return `Greetings, ${name}!`;
10// }
11// export default greetSomeone;
12
13// 預設匯出一個類別
14// export default class MyGreeter {
15//   greeting: string;
16//   constructor(message: string) {
17//     this.greeting = message;
18//   }
19//   greet() {
20//     return "Hello, " + this.greeting;
21//   }
22// }
23
24// 預設匯出一個值
25// export default "This is a default export string";

程式碼解釋:greeter.ts 裡面,我哋使用 export default 關鍵字匯出咗一個 greet 函式。一個模組只能有一個 export default。如果嘗試定義多個,TypeScript 編譯器會報錯。

2.3 匯入 (Importing - import)

使用 import 關鍵字從其他模組匯入功能。路徑通常係相對路徑 (例如 './mathUtils') 或絕對路徑 (較少見,通常透過路徑別名配置)。

2.3.1 匯入命名成員

當匯入命名匯出嘅成員時,你需要使用大括號 {} 將佢哋括起嚟,並且名稱必須同匯出時嘅名稱一致 (除非使用 as 重新命名)。

範例 (main.ts):
typescript
1// main.ts
2// 假設 mathUtils.ts 喺同一個目錄
3import { PI, add, Calculator, MathOperation, Point2D } from './mathUtils';
4// 你可以使用 'as' 關鍵字為匯入嘅成員重新命名,以避免命名衝突或使用更簡短嘅名稱
5import { subtract as minus } from './mathUtils';
6
7console.log(PI); // 輸出: 3.14159
8console.log(add(5, 3)); // 輸出: 8
9console.log(minus(10, 4)); // 輸出: 6 (subtract 被重新命名為 minus)
10
11const calc = new Calculator();
12console.log(calc.multiply(4, 6)); // 輸出: 24
13
14const customAdd: MathOperation = (x, y) => x + y + 1; // 實現 MathOperation 介面
15console.log(customAdd(10, 3)); // 輸出: 14
16
17const p: Point2D = { x:1, y:2 };
18console.log(p); // 輸出: { x: 1, y: 2 }

程式碼解釋: main.ts./mathUtils.ts 模組匯入咗多個命名成員。注意 subtract 函式喺匯入時被重新命名為 minus

2.3.2 匯入預設成員

匯入預設匯出嘅成員時,你唔需要使用大括號,並且可以為匯入嘅成員指定任何你鍾意嘅名稱。

範例 (app.ts):
typescript
1// app.ts
2// 假設 greeter.ts 喺同一個目錄
3import myCustomGreeterFunction from './greeter'; // 匯入預設匯出時,可以自己命名
4// 如果 greeter.ts 預設匯出一個類別,例如:
5// import MyGreeterClass from './greeter';
6
7console.log(myCustomGreeterFunction("TypeScript Modules")); // 輸出: Hello, TypeScript Modules!
8
9// const greeterInstance = new MyGreeterClass("World");
10// console.log(greeterInstance.greet());

程式碼解釋: app.ts./greeter.ts 模組匯入咗預設匯出嘅函式,並將其命名為 myCustomGreeterFunction

2.3.3 匯入整個模組 (* as name)

有時你想將一個模組中所有命名匯出嘅成員都匯入到一個物件中,然後透過呢個物件嚟存取佢哋。

範例 (anotherApp.ts):
typescript
1// anotherApp.ts
2// 假設 mathUtils.ts 喺同一個目錄
3import * as MathLib from './mathUtils'; // 將 mathUtils.ts 中所有命名匯出都放喺 MathLib 物件度
4
5console.log(MathLib.PI); // 輸出: 3.14159
6console.log(MathLib.add(10, 20)); // 輸出: 30
7
8const advancedCalc = new MathLib.Calculator();
9console.log(advancedCalc.multiply(3, 7)); // 輸出: 21
10console.log(advancedCalc.divide(10, 2)); // 輸出: 5

程式碼解釋: anotherApp.ts 使用 import * as MathLib from './mathUtils'mathUtils.ts 模組中所有命名匯出嘅成員(PI, add, subtract, Calculator 等)都匯入到 MathLib 呢個物件度。之後就可以透過 MathLib.memberName 嘅方式嚟使用佢哋。注意,預設匯出 (export default) 唔會被包含喺 * as name 嘅匯入物件中,你需要單獨匯入預設匯出。

2.4 為何使用模組?

使用模組帶嚟好多好處:

  • 封裝 (Encapsulation): 模組內部嘅變數、函式、類別等預設情況下係私有嘅,只可以喺模組內部存取。只有明確使用 export 匯出嘅成員先可以被其他模組存取。呢個有助於隱藏實現細節,只暴露必要嘅接口。
  • 重用 (Reusability): 編寫好嘅模組可以喺唔同嘅專案或專案嘅唔同部分重用,避免重複編寫相同嘅程式碼。
  • 可維護性 (Maintainability): 將應用程式分割成更小、更專注嘅模組,每個模組負責特定嘅功能。咁樣令程式碼更易於理解、修改、測試同除錯。當你需要修改某個功能時,只需要關注相關嘅模組。
  • 避免命名衝突 (Avoiding Namespace Pollution): 每個模組都有自己獨立嘅作用域。喺唔同模組中使用相同嘅變數名或函式名都唔會產生衝突,因為佢哋被限制喺各自嘅模組作用域內。呢個大大減少咗全域變數命名衝突嘅風險。
  • 依賴管理 (Dependency Management): 模組系統令模組之間嘅依賴關係變得清晰。你可以明確知道邊個模組依賴邊啲其他模組,有助於理解程式碼結構同埋進行依賴分析。

🎯 互動練習 5:完整模組系統

創建一個完整嘅模組化應用程式,展示模組系統嘅所有優點:

總結

  • 本課重點回顧 1: 列舉 (Enums) 為一組相關嘅常數值提供友好名稱,有數字列舉、字串列舉同常數列舉。數字列舉支持反向對應,即可以由值搵返名稱。
  • 本課重點回顧 2: 常數列舉 (const enum) 喺編譯時會被內聯,所有引用都會被替換成實際值,可以優化編譯後嘅程式碼體積同輕微提升性能,但執行時期唔存在。
  • 本課重點回顧 3: 模組 (Modules) 係組織同封裝程式碼嘅重要方式,有助於創建大型、可維護嘅應用程式。TypeScript 採用 ES6 模組標準。
  • 本課重點回顧 4: 使用 export 關鍵字可以匯出模組成員。一個模組可以有多個命名匯出 (export const name = ...),但最多只能有一個預設匯出 (export default ...)。
  • 本課重點回顧 5: 使用 import 關鍵字可以從其他模組匯入成員。匯入命名成員時使用 { memberName },匯入預設成員時可以直接指定一個名稱。亦可以使用 import * as Alias from './module' 將所有命名匯出匯入到一個別名物件中。
  • 本課重點回顧 6: 模組化有助於提高程式碼嘅封裝性、重用性、可維護性,並有效避免全域命名衝突,同時令依賴關係更清晰。

練習題

練習 1:工作日列舉

創建一個數字列舉並實現工作日判斷函式:

練習 2:支付方式處理

創建字串列舉並實現支付處理函式:

練習 3:模組化日誌系統

創建一個完整嘅模組化日誌系統:

思考題

  1. 乜嘢情況下你會選擇使用字串列舉而唔係數字列舉?

    • 可讀性: 字串列舉嘅值本身就有意義,例如 Theme.Dark = "dark-theme"Theme.Dark = 1 更容易理解
    • 序列化: 當需要將列舉值保存到 JSON 或發送到 API 時,字串值更有意義
    • 除錯: 喺除錯時,字串值能提供更多上下文信息
  2. 乜嘢情況下 const enum 會係一個好選擇?又有乜嘢限制?

    • 好選擇的情況:
      • 性能敏感嘅應用程式,需要減少 JavaScript 輸出體積
      • 列舉值在編譯時已知且不會動態改變
      • 不需要反向對應功能
    • 限制:
      • 執行時期不存在,無法進行反向對應
      • 不能用於需要動態訪問列舉的場景
      • 成員初始化必須是常數表達式

課程總結與展望

恭喜你!你已經完成咗【TypeScript 初級教學網站】嘅所有課程!喺呢個精彩嘅學習旅程中,你已經掌握咗 TypeScript 嘅核心概念,從最基本嘅類型系統、變數宣告、函式定義,到更進階嘅介面、類別、泛型、強大嘅進階類型,以及今日學到嘅列舉同模組基礎。

你依家已經具備咗使用 TypeScript 編寫更健壯、更可靠、更可維護嘅 JavaScript 應用程式嘅堅實基礎。TypeScript 嘅靜態類型檢查會成為你開發過程中嘅好幫手,幫你及早發現錯誤,提升開發效率同程式碼質素。

接下來可以做啲乜?

  • 實踐!實踐!實踐! 最重要嘅事講三次!嘗試將學到嘅知識應用到你嘅個人專案、工作項目,或者參與開源專案。只有不斷練習,先可以真正鞏固所學。
  • 深入學習 TypeScript 進階特性: TypeScript 仲有好多更深入嘅特性等緊你去發掘,例如:
  • 裝飾器 (Decorators)
  • 命名空間 (Namespaces)
  • 聲明合併 (Declaration Merging)
  • 模組解析策略
  • 編譯器 API
  • 學習相關生態系統: 探索 TypeScript 在不同框架中嘅應用,如 React、Angular、Vue.js 等
  • 參與社群: 加入 TypeScript 社群,分享經驗,學習他人嘅最佳實踐

記住,學習程式設計係一個持續嘅過程。保持好奇心,不斷實踐,你一定能夠成為一個優秀嘅 TypeScript 開發者!


[教學網站首頁連結] | [GitHub 專案連結 (如果有的話)]