# TypeScript Student Cookbook 大網 --- * Ch1:TypeScript 簡介 * Ch2:TypeScript 類型介紹 * Ch3:TypeScript 函數 * Ch4:TypeScript 介面 * Ch5:TypeScript 類別 * Ch6:TypeScript 模組和命名空間 * Ch7:TypeScript 泛型 * Ch8:TypeScript 實戰練習 Ch1:TypeScript 簡介 --- ## 什麼是TypeScript JavaScript 是一個很freestyle的家伙,無法在開發階段宣告變數型別,你無法知道會接到什麼型別的物件,寫的時候很爽,debug 的時候很干。 TypeScript 是由微軟開發的一種開源程式語言,具有強型別概念,最初在2012年推出。TypeScript 是 JavaScript 的超集,也就是說,所有合法的 JavaScript 程式碼在 TypeScript 中都是合法的。TypeScript 的目的是提高 JavaScript 的開發體驗,特別是對於大型專案。提供強型別系統,TypeScript 能夠在開發過程中捕捉錯誤,進而減少生產中的 bug 數量。 **主要特性包括**: * 靜態類型檢查:在編譯時檢查程式碼中的型別錯誤。 * 現代 JavaScript 特性:支持 ES6 及更高版本的語法。 * 強大的工具支持:與大多數 IDE 和編輯器無縫整合,提高開發效率。 ## TypeScript與JavaScript的差異 TypeScript 和 JavaScript 在語法上有很多相似之處,但它們之間有幾個關鍵的差異: 1. **類型系統** * JavaScript:動態類型語言,類型是在運行時檢查的。 * TypeScript:靜態類型語言,類型是在編譯時檢查的,這使得開發過程中能夠早期發現和修正錯誤。 2. **編譯** * JavaScript:直接由瀏覽器或 Node.js 解釋執行。 * TypeScript:需要先編譯成 JavaScript,然後才能在瀏覽器或 Node.js 中執行。 3. **語法擴展** * JavaScript:不支持類型宣告、介面、列舉等功能。。 * TypeScript:需要先編譯成 JavaScript,然後才能在瀏覽器或 Node.js 中執行。 TypeScript 透過引入靜態類型系統和現代 JavaScript 特性,提高開發大型應用程序的可靠性和效率。保持與 JavaScript 的相容性,使開發者可以逐步採用 TypeScript,而不需要完全拋棄現有的 JavaScript 代碼。 以下範例是 JavaScript 和 TypeScript 語法的簡單範例。這個範例示範如何在兩種語言中定義和使用變數,以及如何使用函數。 * **JavaScript** ``` // 定義一個變數 var message = "Hello, world!"; // 定義一個函數 function greet(name) { return "Hello, " + name + "!"; } // 呼叫函數 console.log(greet("JavaScript")); // Output: Hello, JavaScript! ``` * **TypeScript** ``` // 定義一個變數,並加上類型宣告 let message: string = "Hello, world!"; // 定義一個函數,並加上參數和返回值的類型宣告 function greet(name: string): string { return "Hello, " + name + "!"; } // 呼叫函數 console.log(greet("TypeScript")); // Output: Hello, TypeScript! ``` 1. **變數定義**: * JavaScript:使用 var 定義變數,沒有類型宣告。 * TypeScript:使用 let 定義變數,並加上類型宣告 string,確保變數 message 必須是字串。 2. **函數定義**: * JavaScript:函數 greet 沒有參數和返回值的類型宣告。 * TypeScript:函數 greet 加上了參數 name 和返回值的類型宣告 string,確保參數 name 必須是字串,且函數返回值也必須是字串。 這個範例示範 TypeScript 如何通過類型宣告來提高代碼的可讀性和可靠性,並在編譯時檢查類型錯誤,減少執行時的錯誤發生。 ## TypeScript 開發環境準備 #### 安裝VS Code 和 PowerShell * 下載安裝 [VS Code](https://code.visualstudio.com/download) (注意:選擇system install) * 下載安裝 [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4) * VS Code快速鍵 1. F12 Go to : Definition 2. Shift+F12 : Go to References 3. Ctrl+F12 : Go to Implementation * VS Code 外掛工具 1. HTML-Essentials 2. Prettier 3. Vue 3 Snippets #### 安裝 typescript、Node.js 和 npm Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行環境,npm(Node Package Manager)則是 Node.js 的套件管理工具。這兩者是 TypeScript 開發環境的基礎。VS Code支援 TypeScript 語言,但預設並未安裝 TypeScript 編譯器。 * **下載 Node.js 安裝包** 1. 前往 [Node.js 官方網站](https://nodejs.org/en),下載適合你的操作系統的安裝包(建議選擇 LTS 版本,因為它更穩定)。 2. 執行下載的安裝包,按照提示完成安裝。 3. 安裝過程中會自動安裝 npm。 4. 打開命令提示字元(Windows),輸入以下命令來檢查 Node.js 和 npm 是否安裝成功,如果顯示版本號,則說明安裝成功。 ``` node -v ``` ``` npm -v ``` ![image](https://hackmd.io/_uploads/H1ffSx0d0.png) * **安裝 TypeScript** 1. 打開命令提示字元,輸入以下命令來安裝全域 TypeScript ``` npm install -g typescript ``` 2. 輸入以下命令來檢查 TypeScript 是否安裝成功,如果顯示版本號,則說明安裝成功,就可以在任何地方執行 tsc 命令了。使用 TypeScript 編寫的檔案以 .ts 為副檔名。 ``` tsc -v ``` ![image](https://hackmd.io/_uploads/B1BvSl0O0.png) * **TypeScript 配置檔 (tsconfig.json)** TypeScript 使用 tsconfig.json 檔案來配置編譯選項。這個檔案可以讓我們定義編譯器的行為和輸出。 1. 建立 TypeScript 專案 在專案目錄中打開命令提示字元,輸入以下命令來初始化 TypeScript 配置,初始化後,會在專案目錄中生成 tsconfig.json 檔案 ``` tsc --init ``` 2. 配置 tsconfig.json,打開這個tsconfig.json 檔案,你會看到許多預設的配置選項。 ``` { "compilerOptions": { "target": "es2016", // 編譯後的 JavaScript 版本 "module": "commonjs", // 模組系統 "strict": true, // 啟用所有嚴格檢查選項 "esModuleInterop": true, // 啟用與 ES 模組的互操作性 "skipLibCheck": true, // 跳過對定義檔案的型別檢查 "forceConsistentCasingInFileNames": true // 檔名區分大小寫 }, "include": [ "src" // 包含的檔案目錄 ], "exclude": [ "node_modules" // 排除的檔案目錄 ] } ``` 完成以上步驟,你的 TypeScript 開發環境就準備好了。可以開始編寫 TypeScript 代碼,並使用 tsc 命令來編譯 TypeScript 檔案。 #### Hello World TypeScript 建立一個簡單的 TypeScript 專案,了解如何編寫 TypeScript 代碼並編譯成 JavaScript。 1. 建立專案目錄 建一個新的目錄"hello-ts",打開命令提示字元,導航到這個目錄 ``` cd hello-ts ``` 2. 初始化 npm,初始化 npm 後,會生成一個 package.json 檔案,這個檔案記錄了專案所需的所有套件及其版本。這樣可以確保專案在不同的環境中使用相同的依賴,避免版本不一致問題。 ``` npm init -y ``` 3. 使用 tsc --init 建立 tsconfig.json 檔案,並設置 TypeScript 配置。 ``` tsc --init ``` 4. 開啟命令提示字元,輸入以下指令,開啟VS Code。 ``` code . ``` 5. 建一個 src 資料夾,並在其中建立一個 index.ts 檔案。 6. 編寫以下 TypeScript 代碼 ``` // 定義一個變數 let message: string = "Hello, TypeScript!"; // 定義一個函數 function greet(name: string): string { return `Hello, ${name}!`; } // 呼叫函數並輸出結果 console.log(greet("World")); console.log(message); ``` 7. 開啟命令提示字元,編譯 TypeScript 程式碼,這會在專案目錄中生成一個 dist 資料夾,其中包含編譯後的 JavaScript 檔案。 ``` tsc ``` 8. 使用 Node.js 執行編譯後的 JavaScript 檔案。開啟命令提示字元,輸入以下指令。 ``` node dist/index.js ``` 9. 應該會在命令提示字元中看到以下輸出 ``` Hello, World! Hello, TypeScript! ``` #### 關於 tsconfig.json tsconfig.json 是 TypeScript 編譯器的配置檔案,用來指定 TypeScript 編譯器的行為和選項。 ``` { "compilerOptions": { "target": "es2016", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "./dist", "rootDir": "./src", "baseUrl": ".", "paths": { "@app/*": ["src/app/*"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` * compileOnSave 在最上層設定,設為true,可以讓 IDE 編輯器在儲存檔案時根據 tsconfig.json 重新產生編譯後JS檔案。 * compilerOptions 用於配置 TypeScript 編譯器的行為。以下是一些常用的選項: 1. target:指定編譯後的 JavaScript 版本。 2. module:指定生成的模組系統,例如 "module": "commonjs" 3. strict:啟用所有嚴格的型別檢查選項。 4. skipLibCheck:跳過對定義檔案(.d.ts 檔案)的型別檢查。不會檢查引入的函式庫檔案。 5. forceConsistentCasingInFileNames:強制檔案名大小寫一致。 6. outDir:指定編譯後的檔案輸出目錄。 7. rootDir:指定輸入檔案的根目錄。 8. baseUrl:設定模組解析的基礎目錄。 9. paths:設定模組的路徑映射,幫助解決相對路徑的問題。 10. esModuleInterop:兼容模組導入的方式 * include 用於指定要包含的檔案或目錄,支援使用萬用字元(如 * 和 **)。 * exclude 用於指定要排除的檔案或目錄,支援使用萬用字元,預設會排除 node_modules。 **參考配置** ``` { "compileOnSave": false, // 儲存檔案時根據 tsconfig.json 重新產生編譯檔案 "compilerOptions": { "target": "es2016", // 指定編譯生成的 JS 版本 "module": "commonjs", // 指定生成哪種模組 "lib": [], // 編譯需要引入的特定函式庫檔案 "allowJs": true, // 允許編譯 javascript 文件 "checkJs": true, // 報告 javascript 文件中的錯誤 "declaration": true, // 生成對應的 '.d.ts' 文件 "declarationMap": true, // 生成宣告檔案的 sourceMap "sourceMap": true, // 生成相應的 '.map' 文件 "outDir": "./dist", // 指定輸出目錄 "rootDir": "./src", // ts檔案來源 "composite": true, // 是否編譯構建引用項目 "removeComments": true, // 刪除編譯後的所有的註釋 "strict": true, // 啟用所有嚴格類型檢查選項 "noImplicitAny": true, // 在表達式和聲明上有隱含的 any 類型時報錯 "strictNullChecks": true, // 啟用嚴格的 null 檢查 "strictFunctionTypes": true, // 啟用檢查 function 型別 "strictBindCallApply": true, // 啟用對 bind、call、apply 更嚴格的型別檢查 "strictPropertyInitialization": true, // 啟用 class 實例屬性的給值檢查 "noImplicitThis": true, // 當 this 表達式值為 any 類型的時候,生成一個錯誤 "alwaysStrict": true, // 以嚴格模式檢查每個 module,並在每個文件裡加入 'use strict' "noUnusedLocals": true, // 有未使用的變數時,拋出錯誤 "noUnusedParameters": true, // 有未使用的參數時,拋出錯誤 "noImplicitReturns": true, // 檢查function 裡並不是所有的程式碼路徑都有回傳值的錯誤 "skipLibCheck": true, // 不檢查引入的函式庫檔案 "forceConsistentCasingInFileNames": true // 確保檔案的大小寫一致 }, "exclude": [ // 指定需要排除的文件或文件夾 "node_modules" ], "references": [ // 指定相依性的程式路徑 { "path": "./common" } ] } ``` * source map,儲存了原始碼與編譯後程式碼的對應關係之檔案,讓你在開啟Devtool 時,能讓瀏覽器透過載入source map 的方式幫助你定位原始碼位置,方便除錯。 Ch2:TypeScript 類型介紹 --- ## TypeScript 變數宣告 在 TypeScript 中,變數的宣告方式與 JavaScript 類似,但 TypeScript 加入了靜態類型宣告,讓我們可以在編譯時檢查類型。 * **let 宣告變數** ``` let age: number = 25; // 宣告一個數字類型的變數 let name: string = "John"; // 宣告一個字串類型的變數 let isStudent: boolean = true; // 宣告一個布林類型的變數 ``` * **const 宣告常數** 宣告後不可重新給值的變數 ``` const birthYear: number = 1995; // 宣告一個數字類型的常數 const fullName: string = "John"; // 宣告一個字符串類型的常數 ``` ## 基礎類型 TypeScript 是 JavaScript 的超集,因此它包含了 JavaScript 的所有基本類型。 * string 類型用於表示文字資料。在 TypeScript 中,使用單引號、雙引號或反引號來表示字符串。 ``` let message: string = "Hello, TypeScript!"; let name: string = 'John'; let templateString: string = `Hello, ${name}!`; ``` * number 類型用於表示數字,包括整數和浮點數。TypeScript 中的 number 類型支持十進制、十六進制、八進制和二進制的數字表示法。 ``` let decimal: number = 42; let hex: number = 0x2A; let binary: number = 0b101010; let octal: number = 0o52; ``` * boolean 類型用於表示真/假值,即 true 或 false。 ``` let isDone: boolean = false; let isActive: boolean = true; ``` ## 複合類型 * array 類型用於表示元素類型相同的陣列。在 TypeScript 中,我們可以使用兩種方式來表示。 ``` let list: number[] = [1, 2, 3]; let names: string[] = ["Alice", "Bob", "John"]; ``` ``` //使用泛型表示法 let list: Array<number> = [1, 2, 3]; let names: Array<string> = ["Alice", "Bob", "Charlie"]; ``` * tuple 類型用於表示已知元素數量和類型的陣列。這種陣列中各元素的類型可以不同。 ``` let tuple: [string, number]; tuple = ["Alice", 25]; // 正確 // tuple = [25, "Alice"]; // 錯誤:類型不匹配 ``` ## 特殊類型 * any 類型表示任意類型。當我們不確定一個變數會是什麼類型,或者希望跳過類型檢查時,可以使用 any。 ``` let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; // 也可以是 boolean ``` * unknown 類型表示未知類型。它與 any 類似,但更安全。使用 unknown 類型的變數需要在使用前進行類型檢查。 ``` let notSure: unknown = 4; if (typeof notSure === "string") { console.log(notSure.toUpperCase()); // 正確 } ``` * void 類型表示沒有任何類型。當一個函數沒有回傳值時,可以使用 void 類型。 ``` function warnMessage(): void { console.log("This is a warning message"); } ``` * never 類型表示永遠不會有返回值的類型。這通常用於拋出錯誤或無限循環的函數。 ``` function error(message: string): never { throw new Error(message); } function infiniteLoop(): never { while (true) {} } ``` * null 和 undefined 是 JavaScript 中的原始類型。在 TypeScript 中,它們有自己的類型,分別是 null 和 undefined。 ``` let u: undefined = undefined; let n: null = null; ``` #### JavaScript 中的 null 和 undefined * **undefined** 定義:undefined 表示變數已經宣告但尚未給值。 使用情境:通常在一個變數被宣告但未給值時,或者函數沒有回傳值時。 ``` let a; console.log(a); // undefined function doSomething() { // no return statement } console.log(doSomething()); // undefined ``` * **null** 定義:null 表示明確的“空”值或“無”值。 使用情境:通常用來顯示一個變數預期會有一個物件值,但目前還沒有值。 ``` let b = null; console.log(b); // null ``` #### TypeScript 中的 null 和 undefined * **undefined** 在 TypeScript 中,undefined 依舊表示變數已經宣告但尚未賦值。但如果 TypeScript 編譯器配置中啟用了 strictNullChecks,則不能將 undefined 賦值給其他類型的變數,除非明確指定該變數可以是 undefined。 * **null** 在 TypeScript 中,null 依舊表示一個明確的“空”值。但同樣地,啟用 strictNullChecks 後,null 不能賦值給其他類型的變數,除非該變數明確允許 null 值。 ``` // javascript let x; console.log(x); // undefined x = null; console.log(x); // null ``` ``` // TypeScript(無 strictNullChecks) let x: number; console.log(x); // 編譯錯誤:變數未初始化 x = undefined; // 正常 x = null; // 正常 ``` ``` // TypeScript(有 strictNullChecks) let x: number; x = undefined; // 編譯錯誤 x = null; // 編譯錯誤 let y: number | undefined; y = undefined; // 正常 let z: number | null; z = null; // 正常 ``` 總而言之,TypeScript 中,在未啟用 strictNullChecks 的情況下,undefined 和 null 可以指定給任何類型的變數。一旦啟用 strictNullChecks 後,undefined 和 null 只能指定被明確允許的類型。 ## 練習題 - 個人資訊管理 1. 建立專案目錄 2. 於專案目錄輸入以下指令,初始化專案套件管理 ``` npm init -y ``` 3. 初始化專案的typescript 配置檔 ``` tsc --init ``` 4. 修改tsconfig.json配置 ``` "target": "es2022" "module": "commonjs" "rootDir": "./src" "allowJs": true "checkJs": true "outDir": "./dist" "removeComments": true "strict": true "skipLibCheck": true ``` 5. 建立 src 資料夾並建立 index.ts 檔案 6. 開啟 index.ts 檔案,撰寫以下程式碼 ``` // 字串類型 let fullName: string = "John"; let jobTitle: string = "Software Developer"; // 數字類型 let age: number = 30; let salary: number = 80000; // 布林類型 let isFullTime: boolean = true; // 陣列類型 let skills: string[] = ["TypeScript", "JavaScript", "Vue", "Node.js"]; // tuple類型 let address: [string, number] = ["Sanmin Street", 123456]; // 特殊類型 let notSure: any = "Could be anything"; let unused: void = undefined; let u: undefined = undefined; let n: null = null; // 定義一個函數來輸出個人資訊 function printPersonalInfo(name: string, job: string, age: number, salary: number, isFullTime: boolean, skills: string[], address: [string, number]): void { console.log(`Name: ${name}`); console.log(`Job Title: ${job}`); console.log(`Age: ${age}`); console.log(`Salary: $${salary}`); console.log(`Full Time: ${isFullTime}`); console.log(`Skills: ${skills.join(", ")}`); console.log(`Address: ${address[0]}, No. ${address[1]}`); } // 呼叫函數並輸出個人資訊 printPersonalInfo(fullName, jobTitle, age, salary, isFullTime, skills, address); ``` 7. 編譯 TypeScript 程式碼 ``` tsc ``` 8. 運行編譯後的 JavaScript 程式碼 ``` node dist/index.js ``` ## 列舉(Enum) 列舉(Enum)是一種特殊的數據結構,用來表示一組具名常數。在 TypeScript 中,可以使用 enum 關鍵字來定義列舉。 在這個範例中,Direction 列舉包含四個成員:Up、Down、Left 和 Right。預設情況下,這些成員從 0 開始自動遞增。 ``` enum Direction { Up, Down, Left, Right } let currentDirection: Direction = Direction.Up; console.log(currentDirection); // 輸出: 0 ``` * **手動設置列舉值** 這個範例中,Up 被設置為 1,其他成員依次遞增。 ``` enum Direction { Up = 1, Down, Left, Right } console.log(Direction.Up); // 輸出: 1 console.log(Direction.Down); // 輸出: 2 console.log(Direction.Left); // 輸出: 3 console.log(Direction.Right); // 輸出: 4 ``` * 字串列舉 除了數字列舉,TypeScript 還支援字串列舉 ``` enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" } console.log(Direction.Up); // 輸出: UP console.log(Direction.Down); // 輸出: DOWN console.log(Direction.Left); // 輸出: LEFT console.log(Direction.Right); // 輸出: RIGHT ``` * **使用列舉進行條件判斷** 列舉常用於條件判斷,讓程式碼增加可讀性: ``` enum Status { New, // 0 InProgress, // 1 Completed, // 2 Cancelled // 3 } function getStatusDescription(status: Status): string { switch (status) { case Status.New: return "The task is new."; case Status.InProgress: return "The task is in progress."; case Status.Completed: return "The task is completed."; case Status.Cancelled: return "The task is cancelled."; default: return "Unknown status."; } } console.log(getStatusDescription(Status.New)); // 輸出: The task is new. console.log(getStatusDescription(Status.Completed)); // 輸出: The task is completed. ``` ## 型別推論 變數如果沒有明確的宣告型別,那麼 TypeScript 會採用型別推論(Type Inference)推斷出該變數的型別。 ``` const height = 180; // height = "tall"; // 編譯錯誤:不能將 'string' 類型指定給 'number' 類型 let message = "Hello, world!"; // message = 12345; // 編譯錯誤:不能將 'number' 類型指定給 'string' 類型 let isFinished = false; // isFinished = "yes"; // 編譯錯誤:不能將 'string' 類型指定給 'boolean' 類型 ``` 如果在變數宣告時沒有給值且也沒有明確指定類型,該變數將會被推斷為 any 類型。這意味著變數可以為任何類型的值。 ``` let someVar; someVar = "Hello"; // 可以給字串 someVar = 42; // 可以給數字 someVar = true; // 可以給布林值 ``` ``` let someNumber; console.log(typeof someNumber); // 'undefined' someNumber = 10; // someNumber 現在是數字 console.log(typeof someNumber); // 'number' someNumber = "Ten"; // someNumber 現在是字串 console.log(typeof someNumber); // 'string' ``` ## 聯合類型 聯合類型(Union Types)是 TypeScript 中的一種功能,它允許變數可以是多種類型之一。使用聯合類型,你可以在宣告變數時指定多個可能的類型,而不局限於單一類型。這在處理需要靈活類型的情況下非常有用。聯合類型使用垂直線(|)來分隔不同的類型。例如,如果一個變數可以是 string 或 number,可以這樣定義: ``` let value: string | number; value = "Hello"; // 正確 value = 42; // 正確 // value = true; // 錯誤:不能將 'boolean' 類型指定給 'string | number' 類型 ``` * **函數參數使用聯合類型** ``` function printId(id: number | string) { console.log(`Your ID is: ${id}`); } printId(101); // 正確 printId("202"); // 正確 // printId(true); // 錯誤:不能將 'boolean' 類型指定給 'number | string' 類型 ``` 當使用聯合類型時,TypeScript 無法自動推斷變數當前的具體類型。在這種情況下,需要使用類型保護(Type Guards)來確保安全使用變數。 ``` function printValue(value: string | number) { if (typeof value === "string") { console.log(`The string is: ${value.toUpperCase()}`); } else { console.log(`The number is: ${value.toFixed(2)}`); } } printValue("Hello"); // 輸出: The string is: HELLO printValue(3.14159); // 輸出: The number is: 3.14 ``` ## Literal Types 文字類型 該變數或參數只能接受特定指定的值,通常作用於與聯合類型一起使用,例如:只接受一組特定已知值的函數: ``` // 定義一個函數,參數是Literal Types類型的聯合 function printStatus(status: "success" | "error" | "pending") { console.log(`Current status: ${status}`); } // 正確的使用方式 printStatus("success"); // 輸出: Current status: success printStatus("error"); // 輸出: Current status: error printStatus("pending"); // 輸出: Current status: pending // 錯誤的使用方式(會導致編譯錯誤) // printStatus("completed"); // 編譯錯誤:不能將 'completed' 類型指定給 '"success" | "error" | "pending"' 類型 ``` 範例中的 status 參數的類型是 "success" | "error" | "pending",這是一個聯合類型,表示 status 可以是 "success"、"error" 或 "pending" 中的任一個。 Ch3:TypeScript 函數 --- TypeScript 中,函數的定義方式與 JavaScript 非常相似。最基本的函數定義如下,在這個範例中,定義了一個名為 hello 的函數,該函數沒有參數,並且回傳類型是 void,表示沒有回傳值。 ``` function hello(): void { console.log("Hello, TypeScript!"); } hello(); // 輸出: Hello, TypeScript! ``` * 帶參數的函數 這個範例中,函數 hello 接受一個名為 name 的參數,該參數的類型是 string。 ``` function hello(name: string): void { console.log(`Hello, ${name}!`); } hello("Jhon"); // 輸出: Hello, Jhon! ``` * 具有回傳值的函數 在這個範例中,函數 add 接受兩個參數,這兩個參數的類型都是 number,並且該函數的返回值類型也是 number。 ``` function add(a: number, b: number): number { return a + b; } let sum = add(5, 3); console.log(sum); // 輸出: 8 ``` * 綜合範例 在這個綜合範例中,函數 introduce 接受兩個參數 name 和 age,分別是 string 和 number 類型,並且回傳值是 string 類型。 ``` function introduce(name: string, age: number): string { return `My name is ${name} and I am ${age} years old.`; } let introduction = introduce("Jhon", 30); console.log(introduction); // 輸出: My name is Jhon and I am 30 years old. ``` ## 可選參數與預設參數 在 TypeScript 中,可以為函數的參數設置為可選參數或預設參數。 * 可選參數 可選參數使用問號(?)標記,表示該參數可以傳遞也可以不傳遞。在這個範例中,greeting 是可選參數,如果不傳遞該參數,函數會使用預設的問候語。 ``` function hello(name: string, greeting?: string): void { if (greeting) { console.log(`${greeting}, ${name}!`); } else { console.log(`Hello, ${name}!`); } } hello("Jhon"); // 輸出: Hello, Jhon! hello("Bob", "Good morning"); // 輸出: Good morning, Bob! ``` * 預設參數 預設參數允許為函數參數指定一個預設值,如果調用函數時沒有傳遞該參數,則使用這個預設值。在這個範例中,greeting 有一個預設值 "Hello",如果不傳遞該參數,函數會使用這個預設值。 ``` function hello(name: string, greeting: string = "Hello"): void { console.log(`${greeting}, ${name}!`); } hello("Jhon"); // 輸出: Hello, Jhon! hello("Bob", "Good evening"); // 輸出: Good evening, Bob! ``` ## 剩餘參數(Rest Parameters) 在 TypeScript 中,剩餘參數(Rest Parameters)允許將不固定數量的參數傳遞給函數,並將它們存儲在一個陣列中。這使得函數可以處理可變數量的參數,而不用事先知道參數的實際數量。 1. 剩餘參數使用三個點(...)表示,並且必須是函數參數列表中的最後一個參數。 2. 剩餘參數的類型必須是陣列類型,例如 number[]、string[] 等。 在這個範例中,函數 sum 使用剩餘參數 ...numbers 來接收不定數量的數字,並將這些數字存儲在一個陣列中。然後,函數遍歷這個陣列,計算總和並且回傳。 ``` function sum(...numbers: number[]): number { let total = 0; for (let number of numbers) { total += number; } return total; } let result = sum(1, 2, 3, 4, 5); console.log(result); // 輸出: 15 ``` * 範例1 在這個範例中,函數 concatenate 接受一個字串 separator 作為分隔符號,然後使用剩餘參數 ...strings 接收不定數量的字串,並將它們接在一起。 ``` function buildFullName(firstName: string, ...restOfName: string[]): string { return firstName + " " + restOfName.join(" "); } let fullName = buildFullName("John", "Smith"); console.log(fullName); // 輸出: John Smith ``` * 範例2 在這個範例中,函數 introduce 接受一個固定參數 greeting 和一個剩餘參數 ...names。函數遍歷 names 陣列,並使用 greeting 向每個人打招呼。 ``` function introduce(greeting: string, ...names: string[]): void { for (let name of names) { console.log(`${greeting}, ${name}!`); } } introduce("Hello", "John", "Bob", "Charlie"); // 輸出: // Hello, John! // Hello, Bob! // Hello, Charlie! ``` Ch4:TypeScript Interface 介面的使用 --- 介面(Interface)是一種用來定義物件結構的方式,描述了物件應該具有的屬性和方法。介面可以用來強制實現特定的結構,提高程式碼的可讀性和可維護性。 在 TypeScript 中,可以使用 interface 關鍵字來定義介面,介面可以用來描述物件的結構,並強制物件遵守這種結構。 以下是一個簡單的範例: 在這個範例中,定義了一個名為 Person 的介面,描述了物件應該具有的兩個屬性:name(字串類型)和 age(數字類型)。變數 John 遵循 Person 介面的結構,並被賦值為一個符合該結構的物件。 ``` interface Person { name: string; age: number; } let john: Person = { name: "John", age: 25 }; console.log(john.name); // 輸出: John console.log(john.age); // 輸出: 25 ``` * 可選擇性屬性 介面中的屬性可以被定義為可選擇性的,使用問號(?)來標記。 在這個範例中,age 是 Person 介面中的可選屬性,因此物件 carol 可以不包含 age 屬性。 ``` interface Person { name: string; age?: number; // age 是可選屬性 } let carol: Person = { name: "Carol" }; console.log(carol.name); // 輸出: Carol // console.log(carol.age); // 輸出: undefined ``` * 唯讀屬性 介面中的屬性可以被定義為唯讀的,使用 readonly 關鍵字來標記。 在這個範例中,id 是 Person 介面中的只讀屬性,一旦賦值後就不能修改。 ``` interface Person { readonly id: number; name: string; } let carol: Person = { id: 1, name: "carol" }; console.log(carol.id); // 輸出: 1 // carol.id = 2; // 錯誤:因為'id'是唯讀屬性 ``` * 定義函數參數的介面 介面可以用來定義函數參數的結構,提高代碼的可讀性和類型安全性。 在這個範例中,函數 greet 接受一個參數 person,該參數必須符合 Person 介面的結構。 ``` interface Person { name: string; age: number; } function greet(person: Person): void { console.log(`Hello, ${person.name}. You are ${person.age} years old.`); } let bob = { name: "Bob", age: 30 }; greet(bob); // 輸出: Hello, Bob. You are 30 years old. ``` ## 函數類型的介面 介面不僅可以用來描述物件的結構,還可以用來描述函數的結構。 在這個範例中,定義了一個名為 SearchFunc 的函數類型介面,描述了一個接受兩個字串參數並回傳布林值的函數。變數 mySearch 被指定為一個符合這個結構的函數。 ``` interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc = function(source: string, subString: string): boolean { return source.includes(subString); }; console.log(mySearch("Hello, world!", "world")); // 輸出: true console.log(mySearch("Hello, world!", "TypeScript")); // 輸出: false ``` ## 介面繼承 介面可以繼承另一個介面的屬性和方法。 在這個範例中,Employee 介面繼承了 Person 介面,因此 Employee 包含了 Person 的所有屬性以及自己的屬性 employeeId。 ``` interface Person { name: string; age: number; } interface Employee extends Person { employeeId: number; } let john: Employee = { name: "John", age: 28, employeeId: 101 }; console.log(john.name); // 輸出: John console.log(john.age); // 輸出: 28 console.log(john.employeeId); // 輸出: 101 ``` ## 綜合範例 Person 介面包含了一個可選屬性 age、一個唯讀屬性 id 以及一個函數 greet。變數 emily 被賦值為一個符合這個結構的物件,並且實現了 greet 函數。 ``` interface Person { name: string; age?: number; // 可選屬性 readonly id: number; // 唯讀屬性 greet(greeting: string): void; // 函數 } let emily: Person = { name: "Emily", id: 2, greet: function(greeting: string): void { console.log(`${greeting}, my name is ${this.name}.`); } }; emily.greet("Hi"); // 輸出: Hi, my name is Emily. // emily.id = 3; // 錯誤:因為id是唯讀屬性 ``` Ch5:TypeScript 類別(Class)的使用 --- 類別(Class)定義了物件的結構和行為。透過類別,可以建立多個具有相同屬性和方法的實例(物件)。 * **類別(Class)的基本概念** 1. 類別:定義物件的屬性和方法。 2. 類別成員:類中的屬性和方法。 3. 實例:通過類創建的具體物件。 4. 建構函數:一種特殊的函數,用於初始化類別的實例。 5. 繼承:允許一個類別繼承另一個類別的屬性和方法。 ## 定義類別和類別的成員 在 TypeScript 中,使用 class 關鍵字來定義類別。類別的成員包括屬性(變數)和方法(函數)。 在這個範例中,定義了一個 Person 類,該類別有兩個屬性 name 和 age,以及一個方法 greet。使用 new 關鍵字建立了 Person 類別的實例 alice,並設定屬性值,然後調用 greet 方法。 ``` class Person { // 定義屬性 name: string; age: number; // 定義方法 greet(): void { console.log(`Hello, my name is ${this.name}.`); } } // 建立實例 let alice = new Person(); alice.name = "Alice"; alice.age = 30; alice.greet(); // 輸出: Hello, my name is Alice. ``` ## 類別的建構函數 建構函數是一種特殊的函數,用於在建立類別的實例時初始化物件的屬性。在 TypeScript 中,建構函數使用 constructor 關鍵字來定義。 在這個範例中, Person 類別中定義了一個建構函數,該建構函數接受兩個參數 name 和 age,並將它們賦值給類別的屬性。在建立實例時,傳遞這些參數來初始化屬性。 ``` class Person { name: string; age: number; // 定義建構函數 constructor(name: string, age: number) { this.name = name; this.age = age; } greet(): void { console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`); } } // 使用建構函數建立類別的實例 let bob = new Person("Bob", 25); bob.greet(); // 輸出: Hello, my name is Bob and I am 25 years old. ``` ## 類別的繼承 繼承是面向物件導向裡的一個重要概念,允許一個類別繼承另一個類別的屬性和方法。繼承使用 extends 關鍵字來實現。 這個範例中,定義了一個基礎類別 Animal,該類別有一個屬性 name 和一個方法 move。然後,定義了一個子類別 Dog,該子類別繼承了 Animal 類的屬性和方法,並且增加了一個新的方法 bark。 ``` // 定義基礎類別 class Animal { name: string; constructor(name: string) { this.name = name; } move(distance: number): void { console.log(`${this.name} moved ${distance} meters.`); } } // 繼承基礎類別 class Dog extends Animal { bark(): void { console.log("Woof! Woof!"); } } let myDog = new Dog("Buddy"); myDog.bark(); // 輸出: Woof! Woof! myDog.move(10); // 輸出: Buddy moved 10 meters. ``` ## super 關鍵字 使用 super 關鍵字來呼叫基礎類別的建構函數和方法。 這個範例中,Bird 類別繼承了 Animal 類,並且在建構函數中使用 super 調用了 Animal 類別的建構函數。 ``` // 定義基礎類別 class Animal { name: string; constructor(name: string) { this.name = name; } move(distance: number): void { console.log(`${this.name} moved ${distance} meters.`); } } // 繼承基礎類別 class Bird extends Animal { constructor(name: string) { super(name); // 調用基礎類別的建構函數 } fly(distance: number): void { console.log(`${this.name} flew ${distance} meters.`); } } let myBird = new Bird("Polly"); myBird.move(5); // 輸出: Polly moved 5 meters. myBird.fly(15); // 輸出: Polly flew 15 meters. ``` ## 存取器(getter和setter) 在 TypeScript 中,使用 getter 和 setter 來封裝對屬性的訪問。這允許屬性被讀取或設置時執行額外的邏輯。使用 get 關鍵字定義 getter,使用 set 關鍵字定義 setter。 在這個範例中,Person 類有一個私有屬性 name,並使用 getter 和 setter 來封裝對 _name 的訪問。在設置新名稱時,setter 執行額外的檢查以確保名稱不是空字符串。 ``` class Person { private _name: string; constructor(name: string) { this._name = name; } // 定義 getter get name(): string { return this._name; } // 定義 setter set name(newName: string) { if (newName && newName.length > 0) { this._name = newName; } else { console.log("Name must be a non-empty string."); } } } let person = new Person("Alice"); console.log(person.name); // 輸出: Alice person.name = "Bob"; console.log(person.name); // 輸出: Bob person.name = ""; // 輸出: Name must be a non-empty string. console.log(person.name); // 輸出: Bob ``` ## 靜態成員 靜態方法和靜態屬性是屬於類別本身,而不是類別的實例。這意味著可以不需要建立類別的實例就可以訪問靜態方法和靜態屬性。使用 static 關鍵字定義靜態方法和靜態屬性。 ``` class Circle { static pi: number = 3.14; // 靜態屬性 static calculateArea(radius: number): number { return this.pi * radius * radius; // 靜態方法 } } console.log(Circle.pi); // 輸出: 3.14 console.log(Circle.calculateArea(10)); // 輸出: 314 ``` ## public、private和protected TypeScript 可以使用三種訪問修飾詞(Access Modifiers),分別是 public、private 和 protected。 * public 修飾的屬性或方法是公開的,可以在任何地方被訪問,預設所有的屬性和方法都是 public * private 修飾的屬性或方法是私有的,不能在類別的外部被訪問 * protected 修飾的屬性或方法是受保護的,它和 private 類似,區別是它在子類別中是被允許訪問的 ## 類別與介面的差異 * 介面(Interface) 用來定義物件的結構,描述物件應該具備的屬性和方法,但不包含具體的實現,介面主要用來約束物件的形狀、作為類別的類型檢查或作為函數參數和返回值的類型。 * 類別(Class) 用來定義物件的藍圖,包含具體的屬性和方法的實現。類別可以用來建立多個實例,每個實例都有相同的屬性和方法。類別也可以用來繼承和擴展其他類別。 *介面與類別結合使用 介面可以用來約束類別的結構,保證類別實現特定的屬性和方法。 在這個範例中,Person 類別實現了 PersonInterface 介面,這表示 Person 類別必須具有 PersonInterface 介面中定義的所有屬性和方法。 ``` interface PersonInterface { name: string; age: number; greet(): void; } class Person implements PersonInterface { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet(): void { console.log(`Hello, my name is ${this.name}.`); } } let charlie = new Person("Charlie", 28); charlie.greet(); // 輸出: Hello, my name is Charlie. ``` ## 練習題:設計一個簡單的銀行帳戶系統(Lab5-1) 設計一個銀行帳戶系統,包括基本的帳戶資訊和操作,例如:存款、取款和查詢餘額。 1. 建立一個 BankAccount 類別,包含以下屬性 * accountNumber(帳號):字串類型 * accountHolder(帳戶持有人):字串類型 * balance(餘額):數字類型 2. 在 BankAccount 類別中實現以下方法 * deposit(amount: number): void(存款):增加帳戶餘額 * withdraw(amount: number): void(提款):減少帳戶餘額,如果餘額不足,則顯示錯誤訊息 * getBalance(): number(餘額查詢):回傳當前帳戶餘額 ## 練習題:設計一個簡單的電子商務系統(Lab5-2) 1. 建一個 Product 類別,包含以下屬性 * productId(產品編號):字串類型 * name(產品名稱):字串類型 * price(價格):數字類型 * stock(庫存):數字類型 2. 在 Product 類別中實現以下方法 * purchase(quantity: number): void(購買產品):如果庫存充足,減少庫存,否則顯示錯誤訊息 * restock(quantity: number): void(補貨):增加庫存 * getInfo(): string(查詢產品資訊):回傳產品的基本資訊(產品名稱、價格、庫存) Ch6:模組和命名空間 --- ## 模組(Module) 模組(Module)是 TypeScript 中用來組織和封裝程式碼的一種機制。每個模組都是一個獨立的程式碼單元,具有自己的作用域,避免變數和函數之間的命名衝突。模組是有助於提高代碼的可維護性和可重用性。 模組透過導出(export)和導入(import)來使用。export模組中的成員,然後在其他模組中import這些成員。 * **導出(export)** 使用 export 關鍵字來導出模組中的成員,包括變數、函數、類等。 在這個範例中,定義了一個名為 math.ts 的模組,並導出了 add 函數、pi 常數和 Circle 類別。 ``` // file: math.ts export function add(a: number, b: number): number { return a + b; } export const pi: number = 3.14; export class Circle { radius: number; constructor(radius: number) { this.radius = radius; } area(): number { return pi * this.radius * this.radius; } } ``` * **導入(import)** 使用 import 關鍵字來導入其他模組中導出的成員。 在這個範例中,我們從 math 模組中導入了 add 函數、pi 常數和 Circle 類,並使用它們。 ``` // file: main.ts import { add, pi, Circle } from './math'; console.log(add(2, 3)); // 輸出: 5 console.log(pi); // 輸出: 3.14 let myCircle = new Circle(10); console.log(myCircle.area()); // 輸出: 314 ``` * **導入所有成員** 使用 * as 語法導入模組的所有成員。 ``` // file: main.ts import * as MathUtils from './math'; console.log(MathUtils.add(2, 3)); // 輸出: 5 console.log(MathUtils.pi); // 輸出: 3.14 ``` ## 命名空間 命名空間(Namespace)是另一種組織程式碼的方式,通常用於更大型的應用中。命名空間提供了內部模組機制,將程式碼組織在一個命名空間內,避免全局作用域中的名稱衝突。 * 命名空間的定義 使用 namespace 關鍵字來定義命名空間。 在這個範例中,定義了一個名為 Geometry 的命名空間,並在其中定義了 Circle 和 Rectangle 類別。使用 export 關鍵字將類別導出,使它們可以在命名空間外部使用。 ``` namespace Geometry { export class Circle { radius: number; constructor(radius: number) { this.radius = radius; } area(): number { return Math.PI * this.radius * this.radius; } } export class Rectangle { width: number; height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } area(): number { return this.width * this.height; } } } ``` * 使用命名空間 使用點表示法來訪問命名空間中的成員。 在這個範例中,建立了 Circle 和 Rectangle 的實例,並調用了它們的 area 方法。 ``` let myCircle = new Geometry.Circle(10); console.log(myCircle.area()); // 輸出: 314.1592653589793 let myRectangle = new Geometry.Rectangle(10, 20); console.log(myRectangle.area()); // 輸出: 200 ``` * 巢狀命名空間 命名空間可以巢狀,以便細緻地組織程式碼。 在這個範例中, Geometry 命名空間內嵌套了一個 Shapes 命名空間,並在其中定義了 Triangle 類別。 ``` namespace Geometry { export namespace Shapes { export class Triangle { base: number; height: number; constructor(base: number, height: number) { this.base = base; this.height = height; } area(): number { return 0.5 * this.base * this.height; } } } } let myTriangle = new Geometry.Shapes.Triangle(10, 20); console.log(myTriangle.area()); // 輸出: 100 ``` Ch7:TypeScript 泛型 --- 泛型(Generics)用來建立可重用的程式碼元件。使用使用泛型,可以定義一個函數、介面或類別,這些程式碼元件可以處理多種不同的類型,而不需要重複定義。使用泛型時,更可以保持類型的安全性,避免因使用 any 類型帶來的風險。 * 泛型的基本語法 使用尖括號(< >)來表示泛型參數,在這個範例中,identity 函數使用了泛型參數 T,這使得它可以接受任何類型的參數並返回相同類型的結果。 ``` function identity<T>(arg: T): T { return arg; } ``` * 泛型函數 泛型函數是使用泛型參數來定義的函數。這樣的函數就可以處理多種不同的類型,而不需要重複定義。 在這個範例中,identity 函數使用了泛型參數 T,這使得它可以接受任何類型的參數並返回相同類型的結果。 ``` function identity<T>(arg: T): T { return arg; } let output1 = identity<string>("Hello, TypeScript!"); let output2 = identity<number>(123); console.log(output1); // 輸出: Hello, TypeScript! console.log(output2); // 輸出: 123 ``` * 泛型介面 泛型介面是使用泛型參數來定義的介面。這樣的介面可以描述多種不同類型的結構,而不需要重複定義。 這個範例中,Pair 介面使用了兩個泛型參數 T 和 U,這使得它可以描述包含兩個不同類型屬性的物件。 ``` interface Pair<T, U> { first: T; second: U; } let pair: Pair<string, number> = { first: "Hello", second: 123 }; console.log(pair.first); // 輸出: Hello console.log(pair.second); // 輸出: 123 ``` * 泛型約束 有時我們希望限制泛型參數的類型,這時可以使用泛型約束(Generic Constraints)。泛型約束使用 extends 關鍵字來指定泛型參數必須符合某個條件。 這個範例中,logLength 函數使用了泛型約束 T extends HasLength,這表示泛型參數 T 必須具有 length 屬性。 * 範例1 ``` interface HasLength { length: number; } function logLength<T extends HasLength>(arg: T): void { console.log(arg.length); } logLength("Hello, TypeScript!"); // 輸出: 17 logLength([1, 2, 3, 4, 5]); // 輸出: 5 // logLength(123); // 錯誤:數字類型沒有 length 屬性 ``` * 範例2 ``` // 定義一個interface來約束泛型必須具有 `name` 屬性 interface HasName { name: string; } // 使用泛型和泛型約束 function greet<T extends HasName>(obj: T): string { return `Hello, ${obj.name}!`; } // 正確使用:對象具有 `name` 屬性 const person = { name: "Alice", age: 25 }; console.log(greet(person)); // 輸出: "Hello, Alice!" // Error:缺少 `name` 屬性 const car = { model: "Toyota", year: 2020 }; // Error,因為 `car` 不符合 `HasName` 約束 // console.log(greet(car)); // Error: Argument of type '{ model: string; year: number; }' is not assignable to parameter of type 'HasName'. ``` TypeScript 是採用“結構化類型系統”(Structural Type System),是根據它們所具有的結構(屬性和方法)來判斷是否相容的,而不是基於明確的繼承關係。即使 person 沒有明確地宣告它實現了 HasName 這個interface,它仍然具有 name 屬性,而這正是 HasName 這個這個interface所要求的。因此,TypeScript 認為 person 符合 HasName 的結構要求,並允許它作為 greet 函數的參數。 Ch8:TypeScript 實戰練習 --- ## 實戰 - 簡易圖書館管理系統 (Lab8-1) 設計並實作一個簡易的圖書館管理系統 **系統功能包括** 1. 新增書籍 2. 借閱書籍 3. 歸還書籍 4. 列出所有書籍 5. 查詢書籍狀態 **實作要求** 1. 使用類別來定義書籍和圖書館。 2. 使用介面來定義書籍的結構。 3. 使用泛型來實現借閱紀錄的管理。 4. 使用列舉來表示書籍的狀態。 5. 使用模組來組織代碼。 6. 使用命名空間來防止命名衝突。 * **實作步驟** 1. 定義書籍介面和狀態列舉 (models.ts) 2. 定義書籍類別 (book.ts),定義一個 Book 類別來實現 Book 介面。 3. 定義泛型借閱紀錄管理類別(borrowRecord.ts),使用泛型來實現借閱紀錄的管理。 4. 定義圖書館類別(library.ts),定義一個 Library 類別來管理書籍和借閱紀錄。 5. 使用模組和命名空間(main.ts),將所有模組匯入主檔案,並使用命名空間來防止命名衝突。 6. 測試結果,執行 main.ts。 ## 實戰 - 簡易圖書館管理系統網頁版 (Lab8-1) * 學習如何在網頁應用程式中使用 TypeScript。 * 學習如何使用 TypeScript 管理應用程式的狀態。 * 使用 Bootstrap 來進行畫面呈現。 * **功能要求** 1. 顯示圖書列表。 2. 新增新書。 3. 借閱書籍。 4. 歸還書籍。 * **系統結構** ![image](https://hackmd.io/_uploads/ByGsH2lt0.png) * **實作步驟** 1. 建立專案目錄,並安裝 TypeScript 和 Bootstrap ``` mkdir library-app cd library-app npm init -y npm install bootstrap tsc --init ``` 2. 配置 tsconfig.json 編譯選項 ``` { "compilerOptions": { "target": "es2016", "module": "none", "outFile": "./dist/app.js", "rootDir": "./src", "strict": true }, "files": [ "./src/book.ts", "./src/library.ts", "./src/app.ts" ] } ``` 3. 配置 package.json ``` { "name": "library-app", "version": "1.0.0", "main": "index.js", "scripts": { "build": "tsc" }, "dependencies": { "bootstrap": "^5.3.3", "typescript": "^5.5.4" } } ``` 4. 建立src目錄,並新增book.ts,定義書籍類別 ``` // src/book.ts enum BookStatus { Available, Borrowed } class Book { constructor( public id: number, public title: string, public author: string, public status: BookStatus = BookStatus.Available ) {} borrow(): void { if (this.status === BookStatus.Available) { this.status = BookStatus.Borrowed; } } return(): void { if (this.status === BookStatus.Borrowed) { this.status = BookStatus.Available; } } getStatus(): string { return this.status === BookStatus.Available ? 'Available' : 'Borrowed'; } } ``` 5. 新增library.ts,定義圖書館類別 ``` // src/library.ts class Library { private books: Book[] = []; addBook(book: Book): void { this.books.push(book); } getBooks(): Book[] { return this.books; } borrowBook(id: number): void { const book = this.books.find(book => book.id === id); if (book) { book.borrow(); } } returnBook(id: number): void { const book = this.books.find(book => book.id === id); if (book) { book.return(); } } } ``` 6. 新增app.ts,主應用程式邏輯 ``` // src/app.ts const library = new Library(); // 初始化一些書籍 library.addBook(new Book(1, '1984', 'George Orwell')); library.addBook(new Book(2, 'Brave New World', 'Aldous Huxley')); library.addBook(new Book(3, 'Fahrenheit 451', 'Ray Bradbury')); // 渲染書籍列表 function renderBooks(): void { const books = library.getBooks(); const bookList = document.getElementById('book-list'); if (!bookList) { console.error('Could not find book list element'); return; } bookList.innerHTML = ''; books.forEach(book => { const bookItem = document.createElement('tr'); bookItem.innerHTML = ` <td>${book.id}</td> <td>${book.title}</td> <td>${book.author}</td> <td>${book.getStatus()}</td> <td> <button class="btn btn-primary borrow-btn" data-id="${book.id}" ${book.status === BookStatus.Borrowed ? 'disabled' : ''}>Borrow</button> <button class="btn btn-secondary return-btn" data-id="${book.id}" ${book.status === BookStatus.Available ? 'disabled' : ''}>Return</button> </td> `; bookList.appendChild(bookItem); }); // 綁定事件 document.querySelectorAll('.borrow-btn').forEach(button => { button.addEventListener('click', (event) => { const id = (event.target as HTMLButtonElement).getAttribute('data-id'); if (id) { library.borrowBook(Number(id)); renderBooks(); } }); }); document.querySelectorAll('.return-btn').forEach(button => { button.addEventListener('click', (event) => { const id = (event.target as HTMLButtonElement).getAttribute('data-id'); if (id) { library.returnBook(Number(id)); renderBooks(); } }); }); } // 初始化 document.addEventListener('DOMContentLoaded', () => { renderBooks(); // 加新書籍 const addBookForm = document.getElementById('add-book-form') as HTMLFormElement; if (addBookForm) { addBookForm.addEventListener('submit', (event) => { event.preventDefault(); const title = (document.getElementById('title') as HTMLInputElement).value; const author = (document.getElementById('author') as HTMLInputElement).value; const id = library.getBooks().length + 1; const newBook = new Book(id, title, author); library.addBook(newBook); renderBooks(); addBookForm.reset(); }); } else { console.error('Could not find add book form element'); } }); ``` 7. 建立dist目錄,並新增index.html 文件 ``` <!-- file: dist/index.html --> <!DOCTYPE html> <html lang="zh-tw"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Library Management System</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h1 class="mt-5">Library Management System</h1> <form id="add-book-form" class="mt-4"> <div class="mb-3"> <label for="title" class="form-label">Book Title</label> <input type="text" class="form-control" id="title" required> </div> <div class="mb-3"> <label for="author" class="form-label">Author</label> <input type="text" class="form-control" id="author" required> </div> <button type="submit" class="btn btn-primary">Add Book</button> </form> <table class="table table-striped mt-4"> <thead> <tr> <th>ID</th> <th>Title</th> <th>Author</th> <th>Status</th> <th>Actions</th> </tr> </thead> <tbody id="book-list"> <!-- Books will be rendered here --> </tbody> </table> </div> <script src="../dist/app.js"></script> </body> </html> ``` 8. 編譯 TypeScript 程式碼 ``` npm run build ``` 9. VS Code安裝Live Server工具 10. 右鍵Open With Live Server,開啟 dist/index.html 查看網頁應用程式