[Day06] TS:整合前幾天所學,來寫個 Generic Functions 吧!
這幾天的內容中,我們已經學到了幾個重點:
- 泛型(generics)的使用
- 使用
extends
限制泛型 keyof
的使用- Indexed Access Types 的使用
現在讓我們結合這幾天的內容來試著寫個簡單的函式,這個函式名稱是 getObjValue
,功能很簡單,它可以接受兩個參數,第一個參數是物件,第二個參數是該物件中的 key,回傳的內容就是物件中對應到該 key 的 value。最終寫起來會像這樣:
使用方式會像這樣:
const user = {
firstName: 'PJ',
lastName: 'Chen',
age: 35,
isAdmin: true,
};
const product = {
name: 'iPad mini',
price: 14900,
manufacturer: 'Apple',
madeIn: 'China',
};
const isAdmin = getObjValue(user, 'isAdmin'); // true
const manufacturer = getObjValue(product, 'manufacturer'); // 'Apple'
同樣的,如果你原本就已經看得懂上面這個函式的寫法,歡迎直接左轉去看我同事 Kyle 「今晚,我想來點 Web 前端效能優化大補帖!」的精彩文章!。
試著寫出這個函式
首先,讓我們先不管 TypeScript,用原有 JavaScript 的知識寫出這個函式,寫起來會像這樣:
const getObjValue = (obj, key) => obj[key];
但這個函式在沒有型別保護的情況下,很有可能會呼叫到了根本不存在該 obj 中的 key,進而取不到值。例如,我們以為 user 物件中有 title
這個屬性,但實際上卻沒有:
// 在沒有型別保護的情況下,很有可能會呼叫到了根本不存在該 obj 中的 key,進而取不到值
const title = getObjValue(user, 'title'); // undefined
如果我們又直接拿這個 title
去做其他的操作,就有可能會發生錯誤。
現在請讀者試著把它改成 TypeScript 的寫法。
你可以把物件 user
和函式 getObjValue
貼到 TypeScript Playground 中練習看看。
試著定義函式的型別
預設的情況下,如果我們沒有定義參數的型別,它的型別會是 any
:
但這並不是我們想要的情況,因為 any
代表 TypeScript 完全無法掌握這個函式的型別,也同樣無法避免去取到該物件中不存在的屬性。
我們可以用剛剛定義好的 user
物件來想想看要怎麼定義這個函式的型別。首先因為函式的參數 obj
和 key
都需要被明確的給予型別,所以可以:
- 定義
User
型別 obj
可以接受的型別是User
key
需要是 obj 中帶有的屬性 key,
寫起來會像這樣:
// STEP 1:定義 `User` 的型別
type User = {
firstName: string;
lastName: string;
age: number;
isAdmin: boolean;
};
// STEP 2:使用 `User` 的型別來定義 `obj` 和 `key` 的型別
const getObjValue = (obj: User, key: 'firstName' | 'lastName' | 'age' | 'isAdmin') =>
obj[key];
你會注意到,在 key
的型別中,我們列出了所有 User 這個物件型別可能有的屬性名稱('firstName' | 'lastName' | 'age' | 'isAdmin'
),稍微思考一下前幾週所學的內容,應該會發現可以用 keyof
來取代這樣的寫法,否則未來如果 user
的物件有添加新的屬性時,就還需要去修改 key
的型別定義,非常不方便,也不符合 single source of truth 的原則。
因此可以把函式改成這樣:
是不是精簡了不少?
這時候就可以確保使用 getObjValue
的開發者不會發生想要在 user
物件中去取得 title
屬性的情況,因為 TypeScript 會知道 title
這個屬性並不存在 User
中,它會直接報錯:
試著使用泛型
這樣做雖然可以避免開發者誤用不存在 User 型別中的屬性,但因為我們指定了 obj
的型別是 User
,進而導致這個函式變成只能針對 User
型別才能使用,如果 obj
不是 User 的話,就完全沒辦法再使用 getObjValue
。
例如,現在想要取出的是 product 物件中 name
屬性的 value,但因為 product
物件不滿足先前定義的 User
型別,所以 TypeScript 會報錯:
const getObjValue = (obj: User, key: keyof User) => obj[key];
const product = {
name: 'iPad mini',
price: 14900,
manufacturer: 'Apple',
madeIn: 'China',
};
// product 並不滿足 User 的型別
getObjValue(product, 'name'); // ❌ TypeScript compile error