这几天的内容中,我们已经学到了几个重点:
extends
限制泛型keyof
的使用现在让我们结合这几天的内容来试着写个简单的函式,这个函式名称是 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
我们不会想要每当有不同的物件时,就写一个新的、但功能完全一样的函式,这样 getObjValue
就太不好用了:
// 如果不使用泛型...
const getObjValueOfUser = (obj: User, key: keyof User) => obj[key];
const getObjValueOfProduct = (obj: Product, key: keyof Product) => obj[key];
这时候你是否有回忆起前几天提到「泛型」这个好用的东东,让我们试着把上面共同的部分抽出来,变成一个泛型的变数:
可以看到,User
和 Product
就是可以被抽出来变成泛型变数的部分,变成这样:
function getObjValue<T>(obj: T, key: keyof T) {
return obj[key];
}
如此,这个函式就可以同时带入任何的物件而不需要重复定义函式,如果使用者用了物件中不存在的属性时 TypeScript 一样会提出警告:
const user = {
firstName: 'PJ',
lastName: 'Chen',
age: 35,
isAdmin: true,
};
const product = {
name: 'iPad mini',
price: 14900,
manufacturer: 'Apple',
madeIn: 'China',
};
function getObjValue<T>(obj: T, key: keyof T) {
return obj[key];
}
const isAdmin = getObjValue(user, 'isAdmin'); // true
const manufacturer = getObjValue(product, 'manufacturer'); // 'Apple'
如同在第二天针对泛型所提到的,如果没有在
<>
中指定泛型参数的型别,TypeScript 会自动根据带入函式的参数来推导泛型的型别(type argument inference),这就是为什麽这里可以不用写成getObjValue<User>(...)
这种明确告知泛型型别的方式。
上面 getObjValue
一般来说使用上已经没有什麽问题了,但如果我们希望把 key
的型别也变成一个泛型的参数,让使用这个函式的开发者可以自己决定要带入的 key 型别是什麽时(例如,只能取出该物件中的部分属性),可以怎麽做呢?
这时候第一步就是把 key
的型别,也变成一个泛型的参数,这里称作 U
,让使用者有自行决定 key 的型别(U
)的机会:
function getObjValue<T, U>(obj: T, key: U) {
return obj[key];
}
但这时候 TypeScript 会报错:
这个错误的意思是说,TypeScript 没办法确认 U 一定是 T 的 index,换成比较好理解的方式就是,因为 U
太泛了,没办法保证物件 T 中有 U 这个 key 存在。为了要解决这个问题,可以使用在 Day03 学到的泛型限制,透过 extends
来确保 U
一定是物件型别 T 里存在的 key:
就像这样,使用了 U extends keyof T
的方式,来确保泛型 U 一定满足物件型别 T 的 key。
如果我们把滑鼠移到 getObjValue
这个函式时,留意一下它写的这个函式会回传的型别,你会发现它写的是 T[U]
,而这不就是我们昨天提到的 indexed access types 吗?T
是物件型别,U
是物件的 key,T[U]
就是该属性值的型别:
最後,使用者如有需要可以自行指定泛型的型别,并限制 getObjValue
能够取用的 key 的型别:
type Product = {
name: string;
price: number;
manufacturer: string;
madeIn: string;
};
// 限制这里的 getObjValue 只能取用物件中的 'manufacturer' | 'price' 这两个属性
const age = getObjValue<Product, 'manufacturer' | 'price'>(
product,
'manufacturer'
);
如果在定义 key 的型别时,不小心写了原本就不存在物件中的属性时,TypeScript 一样会提早告知错误。例如这里,试着在定义 key 的型别时,取用了不存在 Product 型别中的属性 key age
,TS 就会跳出错误:
这个简单的 getObjValue
函式,就用了多个前几天提到的知识点,如果有对那个部分感到不熟悉,都可以翻阅前几天的内容对照着看。
extends
来限制泛型,但如果能在不限制的形况下就完成想要的功能,就不要给予多余的限制https://tsplay.dev/mqvrQW @ TypeScript Playground
<<: D11-用 Swift 和公开资讯,打造投资理财的 Apps { 台股申购实作.4 - 用 Calendar 物件处理台湾的民国年}
>>: [Android Studio 30天自我挑战] Toast浮动显示快显元件
资讯安全管理制度(ISMS)相关文件属於企业或机构内部资安治理运行标准与纪录,机密等级(控管标准)大...
TF-IDF(Term Frequency - Inverse Document Frequency...
Google在2015年时发表了一篇论文, 提出了FaceNet网路架构。 而其实在前面几天实作人脸...
主要呈现实作成果 以下内容有参考教学影片,底下有附网址。 (内容包括我的不专业解说分析及在实作过程中...
哈罗!昨天使用 SWR 实作了一个小功能,让使用者可以列出某 Github user 的所有公开 r...