TypeScript Learning

變數宣告與作用域

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

第 4 課:變數宣告與作用域

學習目標

  • 掌握 let、const 和 var 的區別及最佳使用場景
  • 理解 TypeScript 中的作用域概念和變數提升
  • 學會在解構賦值中正確使用類型註解
  • 了解塊級作用域、函數作用域和全域作用域
  • 能夠避免常見的作用域相關錯誤

1. 變數宣告方式

1.1 var vs let vs const

在 TypeScript 中,我們有三種變數宣告方式,每種都有其特定的行為和使用場景。

var(不推薦使用)

typescript
1var oldStyle = "這是舊式宣告";

let(可變變數)

typescript
1let mutableValue = "可以改變的值";

const(常數)

typescript
1const immutableValue = "不可改變的值";

1.2 var 的問題

var 有幾個重要的問題,這就是為什麼我們推薦使用 letconst

1.2.1 函數作用域 vs 塊級作用域

typescript
1function demonstrateScope() {
2  if (true) {
3    var varVariable = "我是 var";
4    let letVariable = "我是 let";
5    const constVariable = "我是 const";
6  }
7  
8  console.log(varVariable);    // ✅ 可以訪問
9  // console.log(letVariable); // ❌ 錯誤:塊級作用域外無法訪問
10  // console.log(constVariable); // ❌ 錯誤:塊級作用域外無法訪問
11}

1.2.2 變數提升(Hoisting)

typescript
1function hoistingExample() {
2  console.log(varVariable); // undefined(而不是錯誤)
3  // console.log(letVariable); // ❌ 錯誤:在初始化前無法訪問
4  
5  var varVariable = "var 被提升了";
6  let letVariable = "let 沒有被提升";
7}

1.2.3 重複宣告

typescript
1function redeclarationExample() {
2  var name = "第一次宣告";
3  var name = "第二次宣告"; // ✅ var 允許重複宣告
4  
5  let age = 25;
6  // let age = 30; // ❌ 錯誤:let 不允許重複宣告
7}

1.3 let 的特性

let 提供了更安全的變數宣告方式:

typescript
1// 塊級作用域
2{
3  let blockScoped = "只在這個塊中可見";
4  console.log(blockScoped); // ✅ 可以訪問
5}
6// console.log(blockScoped); // ❌ 錯誤:超出作用域
7
8// 暫時性死區(Temporal Dead Zone)
9function temporalDeadZone() {
10  // console.log(value); // ❌ 錯誤:在初始化前訪問
11  let value = "安全的變數";
12  console.log(value); // ✅ 可以訪問
13}
14
15// 可以重新賦值
16let counter = 0;
17counter = 1; // ✅ 可以重新賦值
18counter = "字串"; // ❌ 錯誤:類型不匹配

1.4 const 的特性

const 用於宣告常數:

typescript
1// 基本類型的常數
2const PI = 3.14159;
3// PI = 3.14; // ❌ 錯誤:不能重新賦值
4
5// 物件常數(引用不可變,內容可變)
6const user = {
7  name: "Alice",
8  age: 25
9};
10
11// user = {}; // ❌ 錯誤:不能重新賦值引用
12user.age = 26; // ✅ 可以修改物件屬性
13
14// 陣列常數
15const colors = ["red", "green", "blue"];
16// colors = []; // ❌ 錯誤:不能重新賦值引用
17colors.push("yellow"); // ✅ 可以修改陣列內容

2. 作用域深入理解

2.1 全域作用域

typescript
1// 全域變數
2let globalVariable = "我是全域變數";
3const GLOBAL_CONSTANT = "我是全域常數";
4
5function accessGlobal() {
6  console.log(globalVariable); // ✅ 可以訪問全域變數
7  console.log(GLOBAL_CONSTANT); // ✅ 可以訪問全域常數
8}

2.2 函數作用域

typescript
1function functionScope() {
2  let functionVariable = "我在函數內";
3  const FUNCTION_CONSTANT = "我也在函數內";
4  
5  function innerFunction() {
6    console.log(functionVariable); // ✅ 可以訪問外層函數變數
7    console.log(FUNCTION_CONSTANT); // ✅ 可以訪問外層函數常數
8    
9    let innerVariable = "我在內層函數";
10  }
11  
12  innerFunction();
13  // console.log(innerVariable); // ❌ 錯誤:無法訪問內層函數變數
14}

2.3 塊級作用域

typescript
1function blockScope() {
2  let outerVariable = "外層變數";
3  
4  if (true) {
5    let blockVariable = "塊級變數";
6    console.log(outerVariable); // ✅ 可以訪問外層變數
7    console.log(blockVariable); // ✅ 可以訪問當前塊變數
8    
9    {
10      let nestedBlockVariable = "嵌套塊變數";
11      console.log(blockVariable); // ✅ 可以訪問外層塊變數
12    }
13    
14    // console.log(nestedBlockVariable); // ❌ 錯誤:無法訪問嵌套塊變數
15  }
16  
17  // console.log(blockVariable); // ❌ 錯誤:無法訪問塊級變數
18}

2.4 模組作用域

typescript
1// 在模組頂層宣告的變數
2let moduleVariable = "模組級變數";
3
4export function getModuleVariable() {
5  return moduleVariable; // ✅ 可以訪問模組變數
6}
7
8// 其他模組無法直接訪問 moduleVariable,除非明確 export

3. 解構賦值與類型註解

3.1 物件解構

typescript
1interface User {
2  id: number;
3  name: string;
4  email?: string;
5  age: number;
6}
7
8const userData: User = {
9  id: 1,
10  name: "Alice",
11  email: "[email protected]",
12  age: 25
13};
14
15// 基本解構(類型推斷)
16const { id, name, email } = userData;
17// id: number, name: string, email: string | undefined
18
19// 解構時重命名
20const { id: userId, name: userName } = userData;
21
22// 解構時提供預設值
23const { email: userEmail = "[email protected]" } = userData;
24
25// 明確類型註解
26const { age }: { age: number } = userData;
27
28// 複雜解構與類型註解
29const { 
30  id: userIdentifier, 
31  name: fullName, 
32  email: contactEmail = "unknown" 
33}: { 
34  id: number; 
35  name: string; 
36  email?: string 
37} = userData;

3.2 陣列解構

typescript
1// 基本陣列解構
2const numbers: number[] = [1, 2, 3, 4, 5];
3const [first, second, third] = numbers;
4// first: number, second: number, third: number
5
6// 跳過元素
7const [firstNum, , thirdNum] = numbers;
8
9// 剩餘元素
10const [head, ...tail] = numbers;
11// head: number, tail: number[]
12
13// 元組解構
14const coordinates: [number, number, string] = [10, 20, "origin"];
15const [x, y, label] = coordinates;
16// x: number, y: number, label: string
17
18// 明確類型註解
19const [xCoord, yCoord]: [number, number] = coordinates;

3.3 嵌套解構

typescript
1interface Address {
2  street: string;
3  city: string;
4  country: string;
5}
6
7interface Person {
8  name: string;
9  address: Address;
10  hobbies: string[];
11}
12
13const person: Person = {
14  name: "Bob",
15  address: {
16    street: "123 Main St",
17    city: "New York",
18    country: "USA"
19  },
20  hobbies: ["reading", "swimming", "coding"]
21};
22
23// 嵌套物件解構
24const { 
25  name: personName, 
26  address: { city, country },
27  hobbies: [firstHobby, ...otherHobbies]
28} = person;
29
30// 帶類型註解的嵌套解構
31const {
32  address: { street }
33}: {
34  address: { street: string }
35} = person;

4. 類型推斷與明確註解

4.1 何時依賴類型推斷

typescript
1// 簡單情況下可以依賴推斷
2let message = "Hello"; // 推斷為 string
3let count = 0; // 推斷為 number
4let isActive = true; // 推斷為 boolean
5
6// 陣列推斷
7let fruits = ["apple", "banana"]; // 推斷為 string[]
8let mixed = [1, "hello", true]; // 推斷為 (string | number | boolean)[]

4.2 何時需要明確註解

typescript
1// 1. 變數未初始化
2let futureValue: string;
3// futureValue = "稍後賦值";
4
5// 2. 需要更寬泛的類型
6let id: string | number = 123;
7id = "user-456"; // ✅ 可以是字串或數字
8
9// 3. 複雜物件結構
10interface Product {
11  id: number;
12  name: string;
13  price: number;
14}
15
16let product: Product = {
17  id: 1,
18  name: "Laptop",
19  price: 999
20};
21
22// 4. 函數參數和返回值
23function calculateTotal(price: number, tax: number): number {
24  return price * (1 + tax);
25}

5. 常見陷阱與最佳實踐

5.1 循環中的作用域問題

typescript
1// 問題:使用 var 在循環中
2function problemWithVar() {
3  var functions: (() => void)[] = [];
4  
5  for (var i = 0; i < 3; i++) {
6    functions.push(function() {
7      console.log(i); // 總是印出 3
8    });
9  }
10  
11  functions.forEach(fn => fn()); // 3, 3, 3
12}
13
14// 解決方案:使用 let
15function solutionWithLet() {
16  const functions: (() => void)[] = [];
17  
18  for (let i = 0; i < 3; i++) {
19    functions.push(function() {
20      console.log(i); // 正確印出 0, 1, 2
21    });
22  }
23  
24  functions.forEach(fn => fn()); // 0, 1, 2
25}

5.2 const 的正確理解

typescript
1// const 保護的是綁定,不是值
2const obj = { value: 1 };
3obj.value = 2; // ✅ 可以修改屬性
4
5const arr = [1, 2, 3];
6arr.push(4); // ✅ 可以修改陣列
7
8// 如果需要深度不可變,使用 readonly
9interface ReadonlyUser {
10  readonly id: number;
11  readonly name: string;
12}
13
14const readonlyUser: ReadonlyUser = { id: 1, name: "Alice" };
15// readonlyUser.name = "Bob"; // ❌ 錯誤:readonly 屬性

5.3 最佳實踐

  1. 優先使用 const,只有在需要重新賦值時才使用 let
  2. 避免使用 var,除非有特殊需求
  3. 在最小的作用域內宣告變數
  4. 使用有意義的變數名稱
  5. 適當使用類型註解提高可讀性

互動練習

練習 1:作用域理解

理解不同宣告方式的作用域行為。

typescript
1type: simple_run
2instruction: 觀察不同變數宣告方式在作用域中的行為差異。
3---
4function scopeDemo() {
5  console.log("=== 作用域演示 ===");
6  
7  // 全域作用域
8  var globalVar = "全域 var";
9  let globalLet = "全域 let";
10  const globalConst = "全域 const";
11  
12  if (true) {
13    // 塊級作用域
14    var blockVar = "塊級 var";
15    let blockLet = "塊級 let";
16    const blockConst = "塊級 const";
17    
18    console.log("在塊內:");
19    console.log("可以訪問 globalVar:", globalVar);
20    console.log("可以訪問 globalLet:", globalLet);
21    console.log("可以訪問 globalConst:", globalConst);
22    console.log("可以訪問 blockVar:", blockVar);
23    console.log("可以訪問 blockLet:", blockLet);
24    console.log("可以訪問 blockConst:", blockConst);
25  }
26  
27  console.log("\n在塊外:");
28  console.log("可以訪問 globalVar:", globalVar);
29  console.log("可以訪問 globalLet:", globalLet);
30  console.log("可以訪問 globalConst:", globalConst);
31  console.log("可以訪問 blockVar:", blockVar); // var 可以訪問
32  
33  // 以下會產生錯誤(在實際 TypeScript 中)
34  // console.log("無法訪問 blockLet:", blockLet);
35  // console.log("無法訪問 blockConst:", blockConst);
36  
37  console.log("✅ 作用域演示完成!");
38}
39
40scopeDemo();
typescript
1// 作用域規則總結:
2
3// 1. var 的特性:
4//    - 函數作用域或全域作用域
5//    - 可以在宣告前訪問(值為 undefined)
6//    - 允許重複宣告
7
8// 2. let 的特性:
9//    - 塊級作用域
10//    - 暫時性死區(不能在宣告前訪問)
11//    - 不允許重複宣告
12
13// 3. const 的特性:
14//    - 塊級作用域
15//    - 暫時性死區
16//    - 不允許重複宣告
17//    - 不允許重新賦值(但物件內容可變)
18
19// 最佳實踐:
20// - 優先使用 const
21// - 需要重新賦值時使用 let
22// - 避免使用 var

練習 2:變數重新賦值與類型安全

學習 TypeScript 如何保護變數的類型安全。

typescript
1type: output_check
2instruction: 完成變數管理系統,展示 letconst 的正確使用。
3expectedOutput: 用戶: Alice (25), 積分: 1500, 設定: {"theme":"dark","notifications":true}
4---
5// 用戶資料管理系統
6class UserManager {
7  // 使用 const 宣告不會改變的配置
8  private readonly CONFIG = {
9    maxScore: 2000,
10    defaultTheme: "light"
11  };
12  
13  // 使用 let 宣告會改變的狀態
14  private currentUser: string | null = null;
15  private userAge: number = 0;
16  private userScore: number = 0;
17  
18  // 使用 const 宣告物件(引用不變,內容可變)
19  private readonly userSettings = {
20    theme: "light",
21    notifications: false
22  };
23  
24  setUser(name: string, age: number): void {
25    this.currentUser = name; // let 變數可以重新賦值
26    this.userAge = age;
27    this.userScore = 0; // 重置積分
28  }
29  
30  updateScore(points: number): void {
31    // 檢查是否超過最大值
32    const newScore = this.userScore + points;
33    if (newScore <= this.CONFIG.maxScore) {
34      this.userScore = newScore; // let 變數可以重新賦值
35    }
36  }
37  
38  updateSettings(theme: string, notifications: boolean): void {
39    // const 物件的屬性可以修改
40    this.userSettings.theme = theme;
41    this.userSettings.notifications = notifications;
42  }
43  
44  getUserInfo(): string {
45    if (!this.currentUser) {
46      return "沒有用戶登入";
47    }
48    
49    // 使用解構賦值獲取設定
50    const { theme, notifications } = this.userSettings;
51    const settings = { theme, notifications };
52    
53    return `用戶: ${this.currentUser} (${this.userAge}歲), 積分: ${this.userScore}, 設定: ${JSON.stringify(settings)}`;
54  }
55}
56
57// 測試用戶管理系統
58const manager = new UserManager();
59manager.setUser("Alice", 25);
60manager.updateScore(1000);
61manager.updateScore(500);
62manager.updateSettings("dark", true);
63
64console.log(manager.getUserInfo());
typescript
1// 變數宣告的最佳實踐:
2
3// 1. const 的使用場景:
4//    - 配置物件(CONFIG)
5//    - 不會重新賦值的引用(userSettings)
6//    - 類別實例(manager)
7
8// 2. let 的使用場景:
9//    - 需要重新賦值的變數(currentUser, userAge, userScore)
10//    - 循環計數器
11//    - 條件性賦值的變數
12
13// 3. readonly 修飾符:
14//    - 防止類別屬性被重新賦值
15//    - 提供額外的類型安全保護
16
17// 4. 解構賦值的好處:
18//    - 簡潔的語法
19//    - 自動類型推斷
20//    - 提高程式碼可讀性
21
22// 記住:const 保護的是綁定,不是值!

練習 3:解構賦值實戰

掌握複雜的解構賦值模式和類型註解。

typescript
1type: output_check
2instruction: 使用解構賦值處理複雜的數據結構,包括嵌套物件和陣列。
3expectedOutput: 公司: TechCorp, CEO: John Doe, 部門: [Engineering,Marketing], 第一個員工: Alice (Engineer)
4---
5// 複雜數據結構
6interface Employee {
7  name: string;
8  position: string;
9  salary: number;
10}
11
12interface Department {
13  name: string;
14  employees: Employee[];
15}
16
17interface Company {
18  name: string;
19  ceo: {
20    firstName: string;
21    lastName: string;
22  };
23  departments: Department[];
24  founded: number;
25}
26
27const companyData: Company = {
28  name: "TechCorp",
29  ceo: {
30    firstName: "John",
31    lastName: "Doe"
32  },
33  departments: [
34    {
35      name: "Engineering",
36      employees: [
37        { name: "Alice", position: "Engineer", salary: 80000 },
38        { name: "Bob", position: "Senior Engineer", salary: 95000 }
39      ]
40    },
41    {
42      name: "Marketing", 
43      employees: [
44        { name: "Carol", position: "Marketing Manager", salary: 75000 }
45      ]
46    }
47  ],
48  founded: 2010
49};
50
51// 使用解構賦值提取數據
52function analyzeCompany(company: Company): string {
53  // 解構公司基本資訊
54  const { 
55    name: companyName,
56    ceo: { firstName, lastName },
57    departments
58  } = company;
59  
60  // 解構部門名稱
61  const departmentNames = departments.map(({ name }) => name);
62  
63  // 解構第一個部門的第一個員工
64  const [firstDepartment] = departments;
65  const { employees: [firstEmployee] } = firstDepartment;
66  const { name: employeeName, position } = firstEmployee;
67  
68  // 組合 CEO 全名
69  const ceoFullName = `${firstName} ${lastName}`;
70  
71  return `公司: ${companyName}, CEO: ${ceoFullName}, 部門: [${departmentNames.join(",")}], 第一個員工: ${employeeName} (${position})`;
72}
73
74// 測試解構賦值
75const result = analyzeCompany(companyData);
76console.log(result);
typescript
1// 解構賦值的高級技巧:
2
3// 1. 嵌套物件解構:
4//    const { ceo: { firstName, lastName } } = company;
5//    直接從嵌套物件中提取屬性
6
7// 2. 陣列解構:
8//    const [firstDepartment] = departments;
9//    提取陣列的第一個元素
10
11// 3. 組合解構:
12//    const { employees: [firstEmployee] } = firstDepartment;
13//    先解構物件屬性,再解構陣列元素
14
15// 4. 重命名:
16//    const { name: companyName } = company;
17//    解構時重命名變數
18
19// 5. 在函數參數中使用解構:
20//    departments.map(({ name }) => name)
21//    直接在 map 回調中解構
22
23// 最佳實踐:
24// - 保持解構模式簡潔易讀
25// - 適當使用類型註解
26// - 避免過度嵌套的解構

練習 4:作用域陷阱避免

學習如何避免常見的作用域相關問題。

typescript
1type: output_check
2instruction: 修復循環中的作用域問題,確保每個函數都能正確捕獲其對應的值。
3expectedOutput: 按鈕 0 被點擊,按鈕 1 被點擊,按鈕 2 被點擊
4---
5// 模擬按鈕點擊處理器
6type ClickHandler = () => void;
7
8class ButtonManager {
9  private handlers: ClickHandler[] = [];
10  
11  // 錯誤的實現(使用 var)
12  createHandlersWrong(count: number): void {
13    this.handlers = [];
14    
15    for (var i = 0; i < count; i++) {
16      this.handlers.push(function() {
17        return `按鈕 ${i} 被點擊`; // 問題:i 總是最終值
18      } as any);
19    }
20  }
21  
22  // 正確的實現(使用 let)
23  createHandlersCorrect(count: number): void {
24    this.handlers = [];
25    
26    for (let i = 0; i < count; i++) { // 使用 let 創建塊級作用域
27      this.handlers.push(() => {
28        return `按鈕 ${i} 被點擊`; // 正確:每個 i 都有自己的作用域
29      } as any);
30    }
31  }
32  
33  // 另一種解決方案:使用立即執行函數
34  createHandlersIIFE(count: number): void {
35    this.handlers = [];
36    
37    for (var i = 0; i < count; i++) {
38      this.handlers.push((function(index) {
39        return function() {
40          return `按鈕 ${index} 被點擊`;
41        };
42      })(i) as any);
43    }
44  }
45  
46  // 現代解決方案:使用 forEach
47  createHandlersModern(count: number): void {
48    this.handlers = [];
49    
50    Array.from({ length: count }, (_, i) => i).forEach(i => {
51      this.handlers.push(() => {
52        return `按鈕 ${i} 被點擊`;
53      } as any);
54    });
55  }
56  
57  simulateClicks(): string {
58    return this.handlers.map(handler => (handler as any)()).join(",");
59  }
60}
61
62// 測試不同的實現
63const manager = new ButtonManager();
64
65// 使用正確的實現
66manager.createHandlersCorrect(3);
67const result = manager.simulateClicks();
68console.log(result);
69
70// 驗證其他實現也能工作
71manager.createHandlersModern(3);
72const modernResult = manager.simulateClicks();
73console.log("現代方法:", modernResult);
typescript
1// 作用域問題的解決方案:
2
3// 1. 問題根源:
4//    var 是函數作用域,在循環中所有迭代共享同一個變數
5//    當回調函數執行時,循環已經結束,i 是最終值
6
7// 2. 解決方案一:使用 let
8//    let 是塊級作用域,每次迭代都創建新的綁定
9//    每個回調函數捕獲自己的 i 值
10
11// 3. 解決方案二:立即執行函數(IIFE)
12//    創建新的函數作用域,將當前的 i 值作為參數傳入
13//    在 ES6 之前的常用解決方案
14
15// 4. 解決方案三:現代方法
16//    使用 Array.from 和 forEach
17//    forEach 的回調函數參數自然形成閉包
18
19// 最佳實踐:
20// - 優先使用 let/const 而不是 var
21// - 理解閉包和作用域的關係
22// - 在循環中創建函數時要特別注意作用域

練習 5:類型註解最佳實踐

學習何時使用明確的類型註解,何時依賴類型推斷。

typescript
1type: output_check
2instruction: 重構程式碼,在適當的地方添加類型註解,在其他地方依賴類型推斷。
3expectedOutput: 處理結果: 成功處理了 3 個項目,跳過了 1 個無效項目
4---
5// 數據處理系統
6interface DataItem {
7  id: number;
8  value: string;
9  isValid: boolean;
10}
11
12interface ProcessResult {
13  processed: number;
14  skipped: number;
15  errors: string[];
16}
17
18class DataProcessor {
19  // 明確註解:公共 API 的參數和返回值
20  processData(items: DataItem[]): ProcessResult {
21    // 依賴推斷:簡單的初始化
22    let processed = 0;
23    let skipped = 0;
24    const errors: string[] = []; // 明確註解:空陣列需要類型資訊
25    
26    // 依賴推斷:forEach 回調參數類型可以推斷
27    items.forEach(item => {
28      if (this.validateItem(item)) {
29        processed++;
30      } else {
31        skipped++;
32      }
33    });
34    
35    // 依賴推斷:返回物件類型可以推斷
36    return {
37      processed,
38      skipped,
39      errors
40    };
41  }
42  
43  // 明確註解:私有方法也建議註解參數和返回值
44  private validateItem(item: DataItem): boolean {
45    // 依賴推斷:簡單的布林運算
46    return item.isValid && item.value.length > 0;
47  }
48  
49  // 明確註解:處理未知類型的數據
50  processUnknownData(data: unknown): ProcessResult | null {
51    // 明確註解:類型守衛需要明確類型
52    if (!this.isDataItemArray(data)) {
53      return null;
54    }
55    
56    // 依賴推斷:類型守衛後類型已確定
57    return this.processData(data);
58  }
59  
60  // 明確註解:類型守衛函數
61  private isDataItemArray(data: unknown): data is DataItem[] {
62    return Array.isArray(data) && 
63           data.every(item => 
64             typeof item === 'object' &&
65             item !== null &&
66             typeof item.id === 'number' &&
67             typeof item.value === 'string' &&
68             typeof item.isValid === 'boolean'
69           );
70  }
71}
72
73// 測試數據處理
74const processor = new DataProcessor();
75
76// 明確註解:複雜的數據結構
77const testData: DataItem[] = [
78  { id: 1, value: "valid data", isValid: true },
79  { id: 2, value: "also valid", isValid: true },
80  { id: 3, value: "valid too", isValid: true },
81  { id: 4, value: "", isValid: false } // 無效項目
82];
83
84// 依賴推斷:函數返回值類型可以推斷
85const result = processor.processData(testData);
86
87// 依賴推斷:字串模板中的表達式
88const message = `處理結果: 成功處理了 ${result.processed} 個項目,跳過了 ${result.skipped} 個無效項目`;
89
90console.log(message);
typescript
1// 類型註解的最佳實踐:
2
3// 何時使用明確註解:
4// 1. 函數參數和返回值(特別是公共 API)
5// 2. 空陣列或物件的初始化
6// 3. 複雜的數據結構
7// 4. 類型守衛函數
8// 5. 處理 unknown 或 any 類型時
9
10// 何時依賴推斷:
11// 1. 簡單的變數初始化
12// 2. 函數內部的局部變數
13// 3. 回調函數參數(當上下文明確時)
14// 4. 返回物件字面量
15// 5. 數學運算和字串操作
16
17// 平衡原則:
18// - 在介面邊界使用明確註解
19// - 在實現細節中依賴推斷
20// - 當推斷不夠精確時使用註解
21// - 為了可讀性而添加註解
22
23// 記住:類型註解是文檔,也是契約!

練習 6:進階解構模式

掌握複雜的解構模式和預設值處理。

typescript
1type: output_check
2instruction: 實現一個配置管理器,使用進階解構模式處理複雜的配置物件。
3expectedOutput: 應用配置: 主題=dark, 語言=zh-TW, 通知=true, API端點=https://api.example.com, 超時=5000ms
4---
5// 配置管理系統
6interface DatabaseConfig {
7  host: string;
8  port: number;
9  username: string;
10  password: string;
11}
12
13interface ApiConfig {
14  endpoint: string;
15  timeout: number;
16  retries?: number;
17}
18
19interface AppConfig {
20  theme: "light" | "dark";
21  language: string;
22  notifications: boolean;
23  database: DatabaseConfig;
24  api: ApiConfig;
25  features?: {
26    beta?: boolean;
27    analytics?: boolean;
28  };
29}
30
31class ConfigManager {
32  private defaultConfig: Partial<AppConfig> = {
33    theme: "light",
34    language: "en-US",
35    notifications: false,
36    api: {
37      endpoint: "https://api.example.com",
38      timeout: 5000
39    },
40    features: {
41      beta: false,
42      analytics: true
43    }
44  };
45  
46  // 使用解構和預設值處理配置
47  processConfig(userConfig: Partial<AppConfig>): string {
48    // 複雜的解構模式,包含預設值
49    const {
50      theme = this.defaultConfig.theme!,
51      language = this.defaultConfig.language!,
52      notifications = this.defaultConfig.notifications!,
53      api: {
54        endpoint = this.defaultConfig.api!.endpoint,
55        timeout = this.defaultConfig.api!.timeout,
56        retries = 3
57      } = {},
58      features: {
59        beta: betaEnabled = this.defaultConfig.features!.beta!,
60        analytics: analyticsEnabled = this.defaultConfig.features!.analytics!
61      } = {}
62    } = userConfig;
63    
64    // 使用解構賦值組織輸出
65    const configSummary = this.formatConfig({
66      theme,
67      language,
68      notifications,
69      endpoint,
70      timeout,
71      retries,
72      betaEnabled,
73      analyticsEnabled
74    });
75    
76    return configSummary;
77  }
78  
79  // 使用解構參數簡化函數簽名
80  private formatConfig({
81    theme,
82    language,
83    notifications,
84    endpoint,
85    timeout
86  }: {
87    theme: string;
88    language: string;
89    notifications: boolean;
90    endpoint: string;
91    timeout: number;
92    retries: number;
93    betaEnabled: boolean;
94    analyticsEnabled: boolean;
95  }): string {
96    return `應用配置: 主題=${theme}, 語言=${language}, 通知=${notifications}, API端點=${endpoint}, 超時=${timeout}ms`;
97  }
98  
99  // 陣列解構處理多個配置源
100  mergeConfigs(...configs: Partial<AppConfig>[]): Partial<AppConfig> {
101    // 使用解構展開運算符合併配置
102    const [baseConfig, ...overrides] = configs;
103    
104    return overrides.reduce((merged, override) => ({
105      ...merged,
106      ...override,
107      // 深度合併嵌套物件
108      api: { ...merged.api, ...override.api },
109      features: { ...merged.features, ...override.features }
110    }), baseConfig || {});
111  }
112}
113
114// 測試配置管理器
115const configManager = new ConfigManager();
116
117// 用戶提供的部分配置
118const userConfig: Partial<AppConfig> = {
119  theme: "dark",
120  language: "zh-TW",
121  notifications: true,
122  api: {
123    endpoint: "https://api.example.com",
124    timeout: 5000
125  }
126};
127
128// 處理配置
129const result = configManager.processConfig(userConfig);
130console.log(result);
typescript
1// 進階解構模式的技巧:
2
3// 1. 嵌套解構與預設值:
4//    const { api: { endpoint = "default" } = {} } = config;
5//    處理可能不存在的嵌套物件
6
7// 2. 重命名與預設值結合:
8//    const { features: { beta: betaEnabled = false } = {} } = config;
9//    解構時重命名並提供預設值
10
11// 3. 函數參數解構:
12//    function formatConfig({ theme, language }: ConfigType)
13//    直接在參數中解構,提高可讀性
14
15// 4. 剩餘參數與解構:
16//    const [first, ...rest] = array;
17//    分離第一個元素和其餘元素
18
19// 5. 展開運算符合併:
20//    { ...base, ...override }
21//    合併物件,後者覆蓋前者
22
23// 最佳實踐:
24// - 為嵌套解構提供預設值
25// - 使用有意義的重命名
26// - 保持解構模式的可讀性
27// - 適當使用類型註解確保安全性

總結

在這一課中,我們深入學習了 TypeScript 的變數宣告和作用域:

關鍵要點

  1. 變數宣告:優先使用 const,需要重新賦值時使用 let,避免使用 var
  2. 作用域理解:掌握塊級作用域、函數作用域和全域作用域的區別
  3. 解構賦值:靈活使用物件和陣列解構,配合適當的類型註解
  4. 類型註解策略:在介面邊界使用明確註解,在實現細節中依賴推斷
  5. 常見陷阱:避免循環中的作用域問題,正確理解 const 的行為

最佳實踐

  • 使用 const 作為預設選擇
  • 在最小的作用域內宣告變數
  • 為公共 API 提供明確的類型註解
  • 善用解構賦值提高程式碼可讀性
  • 理解並避免作用域相關的常見錯誤

下一步

  • 學習函數的定義和類型註解
  • 掌握函數重載和泛型函數
  • 了解箭頭函數和回調函數的類型處理
  • 探索更複雜的類型操作

準備好了嗎?讓我們在下一課深入學習 TypeScript 中的函數!