最近一直在看TS
,看的不少文章都是一些概念,並無什麼實操,或者具體的場景。固然並非說概念不重要。知道與不知道之間是一道鴻溝,可是知道和如何用之間也一樣是這樣,因此我把這一個月學到的一些使用經驗,總結爲一篇文章,具體圍繞一些場景進行思路的梳理。markdown
假設咱們要上傳一個視頻,根據分辨率分紅了320p
、480p
、720p
、1080P
結構長這樣。ssh
type Format320 = { urls: { format320p: string } }
type Format480 = { urls: { format480p: string } }
type Format720 = { urls: { format720p: string } }
type Format1080 = { urls: { format1080p: string } }
// 視頻類型
type Video = Format1080 | Format720 | Format480 | Format320
複製代碼
這時,若是咱們想要經過推導獲得類型VideoType
:format320p | format480p | format720p | format1080p
。應該如何作呢?分佈式
有人可能會說很簡單直接keyof Video['urls']
就能夠了,這樣實際上是不行的,獲取到的結果是never
。ide
爲何?由於Video
是一個聯合類型,那麼Video['urls']
一樣也是一個聯合類型,他們沒有相同的鍵,因此是never函數
/** Video['urls'] = | { format320p: string } | { format480p: string } | { format720p: string } | { format1080p: string } */
type VideoType = keyof Video['urls'] // never
複製代碼
那如何才能拿到咱們想要的鍵的聯合呢,咱們來反推keyof
在什麼狀況下能夠獲得咱們想要的VideoType
ui
type VideoType = keyof {
format320p: string
format480p: string
format720p: string
format1080p: string
} // ok
複製代碼
ok,這樣是能夠的,咱們再進一步推導url
type Urls =
& { format320p: string }
& { format480p: string }
& { format720p: string }
& { format1080p: string }
type VideoType = keyof Urls // ok
複製代碼
哇哦~ 很棒!到這裏咱們已經成功了一半了,咱們能夠很清楚發現,相較以前的Video['urls']
,咱們只須要將聯合類型轉換成交叉類型就能夠了。spa
咱們直接給出轉換方法。code
type UnionToIntersection<T> =
(
T extends any
? (any: T) => any
: never
)
extends (any: infer Q) => any
? Q
: never
複製代碼
要了解這段代碼咱們須要兩個前置條件:orm
貓
在什麼狀況下才是布偶貓
父類的哲學問題 -_-!!! )不瞭解這兩個概念的小夥伴,請先看到這裏,動手去搜搜相關的文章,去夯實基礎,而後再回來繼續通關!
ok,那麼咱們繼續。這個類型的關鍵點在於第二個條件。因爲推斷infer Q
是參數,位於逆變位,TS爲了兼容性會將|
轉換爲&
。又由於第一個條件類型extends any
全部的類型均可以經過,聯合類型別轉換爲了(any: T) => any
,剛好被一樣是參數
的infer Q
推斷出來,這樣就從本來的聯合類型轉變成了交叉類型。
完整代碼以下。
type Format320 = { urls: { format320p: string } }
type Format480 = { urls: { format480p: string } }
type Format720 = { urls: { format720p: string } }
type Format1080 = { urls: { format1080p: string } }
// 視頻類型
type Video = Format1080 | Format720 | Format480 | Format320
type UnionToIntersection<T> =
(T extends any ? (arg: T) => any : never)
extends (arg: infer Q) => any
? Q
: never
type VideoType = keyof UnionToIntersection<Video['urls']>
複製代碼
咱們先看一下這樣的函數
function print(person: object) {
if (typeof person === 'object'
&& person.hasOwnProperty('name')
) {
console.log(person.name) // error, object不存在name屬性
}
}
複製代碼
這是經典的「我知道,可是TS不知道」的案例,雖然咱們經過hasOwnProperty
進行判斷是否有name
屬性,可是TS並不清楚,那咱們要怎麼辦呢?
這裏,咱們須要兩個概念,一個是類型謂詞
,另外一個是交叉類型
。
ok,咱們來寫一個函數,這個函數能夠判斷參數是否是object
,若是是的話,咱們再經過交叉類型
給這個參數從新鍵入(原文爲retype
,英文好的自行理解)新的類型。
function hasOwnProperty< Obj extends object, Key extends PropertyKey >(obj: Obj, key: Key): obj is Obj & Record<Key, unknow> {
return obj.hasOwnProperty(key)
}
複製代碼
這裏的關鍵是obj is Obj & Record<Key, unknow>
,因爲obj is Obj
永遠爲true
,當結果爲true
時TS容許咱們進行從新鍵入類型,這裏咱們經過交叉類型
爲類型添加了對應的Key
屬性,這樣再以後的取值就不會報錯了。頗費特~
完整代碼
function hasOwnProperty< Obj extends object, Key extends PropertyKey >(obj: Obj, key: Key): obj is Obj & Record<Key, unknow> {
return obj.hasOwnProperty(key)
}
function print(person: object) {
if (typeof person === 'object'
&& hasOwnProperty(person, 'name')
// 這裏的person類型變成了object & {name: unknow }
&& typeof person.name === 'string'
) {
console.log(person.name) // ok
}
}
複製代碼
當咱們定義一個對象,而後經過Object.defineProperty
去給它賦值的時候,這時TS是不知道的。
let storage = {
maNumber: 99
}
Object.defineProperty(storage, 'number', {
configurable: true,
writable: true,
enumberable: true,
value: 10,
})
console.log(storage.number) // error! 不存在number屬性
複製代碼
是否是似曾相識的場景,和場景二
同樣,咱們仍是須要經過類型謂詞從新鍵入類型。
可是與場景二
不一樣的是,此次咱們須要考慮一些錯誤狀況,這裏咱們須要的前置知識爲asserts 語句
。
咱們首先先實現一下defineProperty
方法
function defineProperty< Obj extends object, Key extends PropertyKey, PDesc extends PropertyDescriptor >(obj: Obj, key: Key, desc: PDesc): asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.definePropety(obj, key, desc)
}
複製代碼
ok,一樣的套路,先用個必然爲true
的類型謂詞,而後再來個交叉類型。如今咱們只要實現了DefineProperty
就行了。
那咱們爲何要用aseerts
呢?咱們知道對象的值有兩種模式,一種是value
值的模式,另外一種是存取器getter/setter
,若是咱們在descriptor
中同時定義了這兩種類型,JS是會報錯的,因此咱們要經過TS來預檢測。
咱們先實現一下DefineProperty
type DefineProperty<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { writable: any, set(val: any): any } ? never :
Desc extends { writable: any, get(): any } ? never :
Desc extends { writable: false } ? ReadOnly<InferValue<Key, Desc>> :
Desc extends { writable: true } ? InferValue<Key, Desc> :
ReadOnly<InferValue<Key, Desc>>
複製代碼
誒~ 道理我都懂,這個InferValue
是個啥,固然是咱們接下來要實現的類型了!
咱們回過頭來看一下咱們但願DefineProperty
返回個什麼東西,根據場景二
的經驗,咱們須要的是個Record<Key, Value>
。又由於值有兩種類型,那咱們經過InferValue
進行推斷類型就行了。
type InferValue<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { value: any, get(): any } ? never :
Desc extends { value: infer T } ? Record<Key, T> :
Desc extends { get(): infer T } ? Record<Key, T> :
never
複製代碼
大功告成!完整代碼以下
function defineProperty< Obj extends object, Key extends PropertyKey, PDesc extends PropertyDescriptor >(obj: Obj, key: Key, desc: PDesc): asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.definePropety(obj, key, desc)
}
type DefineProperty<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { writable: any, set(val: any): any } ? never :
Desc extends { writable: any, get(): any } ? never :
Desc extends { writable: false } ? ReadOnly<InferValue<Key, Desc>> :
Desc extends { writable: true } ? InferValue<Key, Desc> :
ReadOnly<InferValue<Key, Desc>>
type InferValue<
Key extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { value: any, get(): any } ? never :
Desc extends { value: infer T } ? Record<Key, T> :
Desc extends { get(): infer T } ? Record<Key, T> :
never
let storage = { maxValue: 20 }
defineProperty(storage, 'number', 123)
console.log(storage.number) // ok
複製代碼
最後,給你們來一到經典練習題,ssh昊神
也發過對於這道題的解題思路,我把答案放在底下,你們能夠先本身嘗試嘗試,看看能不能寫出來。
/** * declare function dispatch(arg: Action): void dispatch({ type: 'LOGIN', emialAddress: string }) 轉變成 dispatch('LOGIN', { emialAddress: string }) */
type Action =
| {
type: 'INIT'
}
| {
type: 'SYNC'
}
| {
type: 'LOG_IN',
emialAddress: string
}
| {
type: 'LOG_IN-SUCCESS',
accessToken: string
}
複製代碼
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
type Action =
| {
type: 'INIT'
}
| {
type: 'SYNC'
}
| {
type: 'LOG_IN',
emialAddress: string
}
| {
type: 'LOG_IN-SUCCESS',
accessToken: string
}
declare function dispatch<T>( type: T, action: ): void type ActionType = Action['type'] // 找到對應的type type ExtraAction<A, T> = A extends {type: T} ? A : never
// 去除type屬性
type ExcludeTypeField<A> = {[K in Exclude<keyof A, 'type'>]: A[K]}
// 組合在一塊兒
type ExtractActionParameterWithoutType<A, T> = ExcludeTypeField<ExtraAction<A, T>>
type ExtractSimpleAction<T> = T extends any
? {} extends ExcludeTypeField<T>
? T
: never
: never
type SimpleActionType = ExtractSimpleAction<Action>['type']
type ComplexActionType = Exclude<ActionType, SimpleActionType>
// 重載
declare function dispatch<T extends SimpleActionType>(type: T): void declare function dispatch<T extends ComplexActionType>( type: T, action: ExtractActionParameterWithoutType<Action, T> ): void 複製代碼