# 🏅 Day 14 - keyof 接下來將會著重在如何透過 `type` 型別別名 來產生另一個 `type`,才能讓資料結構變得更有彈性。 在要開始學習 `keyof`、`typeof` 之前,我們要先來講解 `字串字面量 (String Literal)` ## 字串字面量 (String Literal) 型別 它也是用來約束在取值時,只能是某幾個**字串**中的其中一個。 ```tsx= // 不設定為基本型別 type Greeting = "hi" ``` 我們定義 **`Greeting`** 為 ``"hi"`` 這個字串字面量型別時,就表示 **`Greeting`** 型別變數只能賦值為 "hi",不能賦予其他任何 string 的值。例如: ```tsx= let greeting: Greeting; greeting = "hi"; // 正確 greeting = "你好嗎"; // 錯誤: "你好嗎" 不等於 "hi" 型別 ``` ## 字串字面量+聯合型別組合技 接下來的範例,我們就來用字串字面量型別,搭配前面每日任務有提到**聯合型別**做更有力的組合技。 例如一個應用程式允許使用者設定通知方式。我們可以定義一個聯合型別來表示這些通知方式,例如 Email、短信或者 App 通知等等~ ```tsx= // 有發現到這裡的聯合型別,變成字串字面量型別了嗎? type NotificationType = "Email" | "SMS" | "App"; function sendNotification(type: NotificationType, message: string) { switch (type) { case "Email": console.log(`發送郵件:${message}`); break; case "SMS": console.log(`發送訊息:${message}`); break; case "App": console.log(`發送應用通知:${message}`); break; default: console.log("不支援的通知類型"); } } sendNotification("Email", "你有一條新郵件"); sendNotification("SMS", "你有一條新訊息"); sendNotification("App", "你有一個新應用通知"); sendNotification("Line", "你有一個 Line 通知"); // 會出錯,因為 "Line" 沒有符合 NotificationType 其中的字串字面量 ``` ## keyof,取出物件型別裡全部的 key 了解前面觀念後,**`keyof`** 就是能夠將 TypeScript 物件型別的 `key` 通通取出來,轉變成聯合型別+字串字面量,相當的方便~ 我們直接上範例 ```tsx= interface Person { name: string; age: number; hasPet: boolean; } // 取出所有 key 變成聯合型別 type PersonKeys = keyof Person; // "name" | "age" | "hasPet" ``` 你或許會想那能反過來取出每個 `key` 對應的型別嗎? 答案是可以的,我們可以用 **Indexed Access Type** 來達成效果 ### Indexed Access Type - 取出 key 裡面的型別 ```tsx= interface Person { name: string; age: number; hasPet: boolean; } type NameOfPerson = Person["name"] // 取出 string // 也可以搭配聯合型別取多個 // 取出 string | boolean 的聯合型別 type NameAndHasPetOfPerson = Person["name" | "hasPet"] // 殺手技 - 使用 keyof 撈出全部~ // 取出全部型別 string | number | boolean type valuesOfPerson = Person[keyof Person] ``` 當需要撈出所有物件型別中,每的`key` 對應的型別時,就可以用 keyof + indexed access types 組合技來達成嘍 :D ## 總整理 1. 泛型用法、泛型約束 2. type 泛型運用 3. keyof 用法 針對過去幾天的每日任務,我們來設計一個**物件取值**的 TypeScript 的版本 ```tsx= // 泛型函式,取得物件的指定屬性值 function getObjValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // 商品介面 interface Product { name: string; price: number; stock: number; } // 建立一個商品,符合商品介面 const product: Product = { name: '筆記型電腦', price: 20000, stock: 50 }; // 使用 getObjValue 函式來取得不同的商品屬性 const productName = getObjValue(product, 'name'); const productPrice = getObjValue(product, 'price'); const productStock = getObjValue(product, 'stock'); // 在控制台中輸出商品的資訊 console.log(`商品名稱: ${productName}`); console.log(`商品價格: ${productPrice} 元`); console.log(`商品庫存: ${productStock} 個`); ``` 像如果再加上使用者 `user` 的部分,你也可以發現到 `getObjValue` 泛型函式也可以發揮到功用。 ```tsx= // 使用範例 interface User { name: string; age: number; isAdmin: boolean; } const user: User = { name: '張三', age: 30, isAdmin: true }; // 檢查 isAdmin 條件 const isAdmin = getObjValue(user, 'isAdmin'); if (isAdmin) { console.log(`使用者 ${user.name} 是管理員。`); // 在此執行作為管理員需要執行的程式碼 } else { console.log(`使用者 ${user.name} 不是管理員。`); // 在此執行非管理員的相關處理 } ``` 在這裡我們來介紹本情境的泛型函式 ```tsx= // 泛型函式,取得物件的指定屬性值 function getObjValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } ``` - **`T`**:是一個型別參數,代表任何一種物件型別。在使用 **`getObjValue`** 函式時,**`T`** 會被替換成實際傳入的物件的型別。 - **`K extends keyof T`**:這是第二個型別參數,它限制了 **`K`** 必須是 **`T`** 的 `key`(即屬性名稱)之一 ### 函式參數 - **`obj: T`**:這是函式的第一個參數,表示任何一個 **`T`** 型別的物件。 - **`key: K`**:這是函式的第二個參數,代表 **`obj`** 物件中的一個`key`(屬性名)。確保了傳入的 **`key`** 必須是物件 **`obj`** 中存在的屬性。 ### 回傳值 - **`T[K]`**:這表示函式的回傳值將與物件 **`obj`** 中 **`key`** 的值的型別相同。也就是利用到 **Indexed Access Types** 的技巧,允許我們根據 **`key`** 來獲得對應的值型別。 這樣的程式碼,有以下的好處: 1. **型別安全性提高**:透過限制 **`key`** 必須屬於特定物件的屬性,確保在編譯時期就捕捉到任何可能的錯誤,例如嘗試訪問不存在的屬性。 2. **重用性變高,程式碼也簡化許多**:**`getObjValue`** 是一個高度可重用的泛型函式。可以用於任何物件和屬性,而不僅限於某一個特定的介面或型別。像這範例中,`產品` 與 `使用者` 就一起共用到 **`getObjValue`**。 ## 實作題 ### 1. 處理課程與學生資料邏輯 以下題目,**請嘗試用你認知的泛型技巧來進行優化,目前程式碼問題如下:** 1. **型別安全性降低**:沒有用介面或型別別名來定義物件資料結構,直接操作物件和其屬性,沒辦法享受型別檢查的好處 2. **程式碼重複性高**:都對多個物件進行相同的操作(例如取出某個屬性的值),都要為每個物件都寫一遍相同的程式碼,會導致程式碼的重複性高 3. **可讀性、可維護性差也不好**:觀看各個程式碼邏輯,感覺都是各寫各的操作物件和其屬性,要花更多心力去個別維護 4. **變得容易出錯**:綜合以上三點,在初期因為偷懶沒定義好資料結構(interface、type、泛型),以致於更容易在修改程式碼時出錯 因此,請設計一個 **`getObjValue`** 方法,讓取出物件屬性值的時候可以安全的進行操作,並且有型別的支援以提升可讀性和可維護性~ ```tsx= // 建立一個學生物件 const student = { name: '小明', age: 20, major: '資訊工程' }; // 直接從學生物件中取出各個屬性的值 const studentName = student['name']; const studentAge = student['age']; const studentMajor = student['major']; // 在 console 中輸出學生的資訊 console.log(`學生姓名: ${studentName}`); console.log(`學生年齡: ${studentAge}`); console.log(`學生主修: ${studentMajor}`); // 建立一個課程物件 const course = { title: '程式設計', credits: 3, isRequired: true }; // 直接從課程物件中取出各個屬性的值 const courseTitle = course['title']; const courseCredits = course['credits']; const courseIsRequired = course['isRequired']; console.log(`課程名稱: ${courseTitle}`); console.log(`學分數: ${courseCredits}`); console.log(`是否為必修: ${courseIsRequired ? '是' : '否'}`); // TODO: getObjValue 方法 function getObjValue // 範例使用 const studentName = getObjValue(student, "name"); const courseIsRequired = getObjValue(course, "isRequired"); ``` ### 2. 寵物資訊查詢工具 這是一個正在開發的寵物資訊查詢工具,目前已完成的內容如下: ```tsx // 定義寵物資訊型別 type Pet = { id: string; name: string; species: "dog" | "cat"; age: number; owner: string; }; // 寵物列表 (可自行新增) const pets: Pet[] = [ { id: "P1", name: "Coco", species: "dog", age: 3, owner: "Amy" }, { id: "P2", name: "Mimi", species: "cat", age: 2, owner: "Tom" }, { id: "P3", name: "Lucky", species: "dog", age: 5, owner: "John" }, ]; ``` 根據需求,你需要實作以下**三個功能**來完善這個查詢工具,請運用先前所學習到的 TypeScript 以及本日的 `keyof` 特性來完成。 ```tsx // 1. getPetProperty // 輸入:寵物以及欲查詢的屬性 // 輸出:該寵物對應的屬性值 // 範例使用: console.log(getPetProperty(pets[0], "name")); // "Coco" console.log(getPetProperty(pets[1], "owner")); // "Tom" // 2. filterPetsByKey // 輸入:欲篩選的屬性以及屬性值 // 輸出:符合篩選條件的所有寵物列表 // 範例使用: console.log(filterPetsByKey("species", "dog")); // 寵物 species 為 dog 的列表 console.log(filterPetsByKey("owner", "Tom")); // 只顯示 owner = Tom 的寵物 // 3. updatePet // 輸入:目標寵物 id、欲更新的寵物屬性以及屬性值 // 輸出:更新後的寵物 // Bonus:嘗試透過型別限制防止該函式更新 id 的值 // 範例使用: console.log(updatePet("P1", "owner", "Bella")); // 更新 owner console.log(updatePet("P2", "age", 3)); // 更新 age ``` <!-- 解答: 1. function getObjValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // 建立一個學生物件 const student = { name: '小明', age: 20, major: '資訊工程' }; const studentName = getObjValue(student, "name"); const studentAge = getObjValue(student, "age"); const studentMajor = getObjValue(student, "major"); // 建立一個課程物件 const course = { title: '程式設計', credits: 3, isRequired: true }; const courseTitle = getObjValue(course, "title"); const courseCredits = getObjValue(course, "credits"); const courseIsRequired = getObjValue(course, "isRequired"); 2. (1) function getPetProperty<K extends keyof Pet>(pet: Pet, key: K): Pet[K] { if (!pet){ throw new Error("查詢不到寵物"); } return pet[key]; } (2) function filterPetsByKey<K extends keyof Pet>(key: K, value: Pet[K]): Pet[] { return pets.filter(pet => pet[key] === value); } (3) function updatePet<K extends keyof Pet>(id: string, key: K, value: Pet[K]): Pet { const target = pets.find(pet => pet.id === id); if (!target){ throw new Error("查詢不到寵物"); } target[key] = value; return target; } (3).Bonus function updatePet<K extends Exclude<keyof Pet, "id">>(id: string, key:K, value:Pet[K]): Pet | undefined { const target = pets.find(pet => pet.id === id) if (!target){ console.log("查詢不到寵物") return undefined } target[key] = value return target } -->