# 🏅 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" 型別 ``` 在要開始學習 `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 ## 字串字面量+聯合型別組合技 接下來的範例,我們就來用字串字面量型別,搭配前面每日任務有提到**聯合型別**做更有力的組合技。 例如一個應用程式允許使用者設定通知方式。我們可以定義一個聯合型別來表示這些通知方式,例如 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. **型別安全性降低**:沒有用介面或型別別名來定義物件資料結構,直接操作物件和其屬性,沒辦法享受型別檢查的好處,那幹嘛用 TypeScript 😂? 2. **程式碼重複性高**:都對多個物件進行相同的操作(例如取出某個屬性的值),都要為每個物件都寫一遍相同的程式碼,會導致程式碼的重複性高 🤮 3. **可讀性、可維護性差也不好**:觀看各個程式碼邏輯,感覺都是各寫各的操作物件和其屬性,要花更多心力去個別維護 🤧 4. **變得容易出錯**:綜合以上三點,在初期因為偷懶沒定義好資料結構(interface、type、泛型),以致於更容易在修改程式碼時出錯,而懷疑人生 🥺 ```tsx= // 建立一個學生物件 const student = { name: '小明', age: 20, major: '資訊工程' }; // 直接從學生物件中取出各個屬性的值 const studentName = student['name']; // 這裡 studentName 會是 any 型別 const studentAge = student['age']; // 這裡 studentAge 會是 any 型別 const studentMajor = student['major']; // 這裡 studentMajor 會是 any 型別 // 在 console 中輸出學生的資訊 console.log(`學生姓名: ${studentName}`); console.log(`學生年齡: ${studentAge}`); console.log(`學生主修: ${studentMajor}`); // 建立一個課程物件 const course = { title: '程式設計', credits: 3, isRequired: true }; // 直接從課程物件中取出各個屬性的值 const courseTitle = course['title']; // 這裡 courseTitle 會是 any 型別 const courseCredits = course['credits']; // 這裡 courseCredits 會是 any 型別 const courseIsRequired = course['isRequired']; // 這裡 courseIsRequired 會是 any 型別 console.log(`課程名稱: ${courseTitle}`); console.log(`學分數: ${courseCredits}`); console.log(`是否為必修: ${courseIsRequired ? '是' : '否'}`); ``` ## 回報流程 將答案寫在 CodePen,並貼至底下回報就算完成了喔! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答: --> 回報區 --- | Discord | CodePen / 答案 | |:-------------:|:----------------------------------------------------------------:| |洧杰|[Codepen](https://codepen.io/hexschool/pen/poYgYqW?editors=1010)| |苡安|[Codepen](https://codepen.io/yi-an-yang/pen/gOEGGZB)| |HsienLu|[CodePen](https://codepen.io/Hsienlu/pen/XWGeeBK?editors=1111)| |展誠|[CodePen](https://codepen.io/hedgehogkucc/pen/oNVGGPG?editors=1010)| |AndyTsai|[CodePen](https://codepen.io/qdandy38/pen/WNmZXzw?editors=1012)| |LinaChen|[CodePen](https://codepen.io/LinaChen/pen/bGZoYQp)| |YC|[HackMD](https://hackmd.io/SKoJd3EsTlitnjzCx4Rarg)| |精靈|[CodePen](https://codepen.io/justafairy/pen/zYbEpMV)| |clairechang|[Notion](https://claire-chang.notion.site/Day-14-keyof-cfcb4943323d453683e0ded27b028e6e)| |77_0411|[CodePen](https://codepen.io/chung-chi/pen/xxBXYer?editors=0011)| |hiYifang|[HackMD](https://hackmd.io/@gPeowpvtQX2Om6AmD-s3xw/r1dPCijta)| |Henry_Wu|[Codepen](https://codepen.io/hekman1122/pen/KKEXRee?editors=0011)| |deedee1215|[Codepen](https://codepen.io/diddy032/pen/yLwzRar)| |wendy_.li|[HACKMD](https://hackmd.io/PcmFgqZwRd-4Ep3-LgK5_Q)| |Bryan Chu|[CodePen](https://codepen.io/bryanchu10/pen/gOEGQoN)| |jasperlu005|[Codepen](https://codepen.io/uzzakuyr-the-reactor/pen/MWxELON?editors=1011)| |yunhung|[Codepen](https://codepen.io/ahung888/pen/ZEPXwoY?editors=0011)| |hannahTW|[Codepen](https://codepen.io/hangineer/pen/YzgrMay?editors=0011)| |Kai|[Codepen](https://codepen.io/kaiyuncheng-the-styleful/pen/LYazKLm?editors=0012)| |hannahpun|[Codepen](https://codepen.io/hannahpun/pen/WNmXvpa)| |銀光菇|[Codepen](https://codepen.io/genesynthesis/pen/oNVoqRa)| |Lisa|[Codepen](https://codepen.io/lisaha/pen/LYaOrOZ?editors=1012)| |Mi|[CodePen](https://codepen.io/Mi-Jou-Hsieh/pen/BabmPNv?editors=1111)| |Otis|[CodePen](https://codepen.io/humming74/pen/WNmXLOp?editors=1112)| |rikku1756|[CodePen](https://codepen.io/rikkubook/pen/LYaeWKw?editors=1011)| |Amberhh| [codepen](https://codepen.io/Amberhh/pen/wvOPKgL?editors=0011)| |wei|[codePen](https://codepen.io/jweeei/pen/xxBpjQm)| |神奇海螺|[codePen](https://codepen.io/ksz54213/pen/poYpOzb?editors=1111)| |連小艾|[codepen](https://codepen.io/bolaslien/pen/MWxrMoW?editors=1012)| |BonnieChan|[CodePen](https://codepen.io/Bonnie-chan-the-bold/pen/LYaQEdX?editors=0012)| |erwin阿瀚|[CodePen](https://codepen.io/yohey03518/pen/mdoXxGK) |JC|[Codepen](https://codepen.io/jcsamoyed/pen/GReQdxb?editors=0012) |薏慈|[CodePen](https://codepen.io/its_wang/pen/BabxgBQ) |皓皓|[HackMD](https://hackmd.io/@cutecat8110/rkt_OXRca)| |leave3310|[CodePen](https://codepen.io/leave3310-the-looper/pen/jOJeMRe?editors=0010)| |Tori|[HackMD](https://hackmd.io/OAdkiOH-S_WkD-LF6IONVA?view)| |我是泇吟|[CodePen](https://codepen.io/kljuqbxs/pen/YzooxzZ)|