---
# System prepended metadata

title: "\U0001F3C5 Day 14 -  keyof "
tags: [TypeScript 30 天證書培訓營 - 2024]

---

# 🏅 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)|