類型體操之 Chainable 工具類型
在本次挑戰(zhàn)中,你需要定義一個(gè) Chainable 接口,它包含 option(key, value) 和 get 兩個(gè)方法。在 option 方法中,你需要使用給定的 key 和 value 擴(kuò)展當(dāng)前 config 對(duì)象的類型,通過 get 方法來獲取最終結(jié)果。
假設(shè) key 僅接受字符串,而 value 可以是任何值。
interface Chainable = {}
declare const config: Chainable
const result = config
.option('name', 'semlinker')
.option('age', 30)
.option('email', { value: 'semlinker@gmail.com' })
.get()
// expect the type of result to be:
interface Result {
name: string
age: number
email: {
value: string
}
}
Solution
首先我們根據(jù)類型挑戰(zhàn)的要求,來定義一個(gè)基礎(chǔ)的 Chainable 接口。option 方法返回 Chainable 類型以支持鏈?zhǔn)秸{(diào)用。
interface Chainable {
option(key: string, value: any): Chainable
get(): Chainable
}
有了上述的接口之后,TypeScript 編譯器會(huì)推斷出 result 的類型是 Chainable 類型。很明顯這并不符合要求。其實(shí),result 的最終類型,是由用戶的使用方式來決定的,所以我們也無法明確 result 的類型。這時(shí),我們可以定義一個(gè)泛型變量 T 來表示 get 方法返回的類型。
interface Chainable<T> {
option(key: string, value: any): Chainable<T>
get(): T
}
因?yàn)?nbsp;get 方法最終返回的是對(duì)象類型,因此我們使用 TypeScript 泛型約束來約束泛型變量的類型,同時(shí)為該泛型變量設(shè)置一個(gè)默認(rèn)值:
interface Chainable<T extends object = {}> {
option(key: string, value: any): Chainable<T>
get(): T
}
使用了新的 Chainable 接口之后,result 的類型返回的是 {} 類型,還不滿足類型挑戰(zhàn)的要求。接下來,我們從簡單的代碼入手,來分析如何繼續(xù)完善 Chainable 接口的代碼:
declare const config: Chainable
const result = config
.option('name', 'semlinker')
.get()
對(duì)于以上代碼,我們希望 TypeScript 編譯器能推斷出 result 的類型是 { name: string } 類型。因此,我們需要獲取 option 方法中 key 和 value 的類型。因?yàn)?nbsp;option 方法中的 key 和 value 是由用戶動(dòng)態(tài)設(shè)置的,我們也無法提前知道它們的類型,所以我們?cè)俅味x兩個(gè)泛型變量 Key 和 Value 來表示它們的類型。因?yàn)轭愋吞魬?zhàn)中,要求 key 的類型是字符串類型,所以我們使用泛型約束來約束泛型變量 Key 的類型。
interface Chainable<T extends object = {}> {
option<Key extends string, Value>(key: Key, value: Value): Chainable<T>
get(): T
}
定義了 Key 和 Value 泛型變量之后,我們就可以更新 option 方法的返回值類型:
interface Chainable<T extends object = {}> {
option<Key extends string, Value>(key: Key, value: Value):
Chainable<T & { Key: Value }>
get(): T
}
更新完 Chainable 接口后,你會(huì)發(fā)現(xiàn) TypeScript 編譯器推斷出 result 的類型如下:
const result: {
Key: string;
}
很明顯這還是不能滿足類型挑戰(zhàn)的要求,那么如何解決上述的問題呢?這時(shí),你可以利用 TypeScript 映射類型。如果你對(duì) TypeScript 映射類型不熟悉的話,可以閱讀這篇文章。
interface Chainable<T extends object = {}> {
option<Key extends string, Value>(key: Key, value: Value):
Chainable<T & { [P in Key]: Value }>
get(): T
}
之后,TypeScript 編譯器就能正常推斷出 result 對(duì)象的類型了:
const result: {
name: string;
}
當(dāng)然,你也可以使用 TypeScript 內(nèi)置的 Record 工具類型來實(shí)現(xiàn)同樣的功能:
interface Chainable<T extends object = {}> {
option<Key extends string, Value>(key: Key, value: Value):
Chainable<T & Record<Key, Value>>
get(): T
}