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