# 🏅 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,並貼至底下回報就算完成了喔!
解答位置請參考下圖(需打開程式碼的部分觀看)

<!-- 解答:
-->
回報區
---
| 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)|