TypeScript學習、實戰總結

前言

最近一直在看TS,看的不少文章都是一些概念,並無什麼實操,或者具體的場景。固然並非說概念不重要。知道與不知道之間是一道鴻溝,可是知道和如何用之間也一樣是這樣,因此我把這一個月學到的一些使用經驗,總結爲一篇文章,具體圍繞一些場景進行思路的梳理。markdown

例子

場景1、聯合類型轉爲交叉類型

假設咱們要上傳一個視頻,根據分辨率分紅了320p480p720p1080P結構長這樣。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
複製代碼

這時,若是咱們想要經過推導獲得類型VideoTypeformat320p | format480p | format720p | format1080p。應該如何作呢?分佈式

有人可能會說很簡單直接keyof Video['urls']就能夠了,這樣實際上是不行的,獲取到的結果是neveride

爲何?由於Video是一個聯合類型,那麼Video['urls']一樣也是一個聯合類型,他們沒有相同的鍵,因此是never函數

/** Video['urls'] = | { format320p: string } | { format480p: string } | { format720p: string } | { format1080p: string } */
type VideoType = keyof Video['urls']  // never
複製代碼

那如何才能拿到咱們想要的鍵的聯合呢,咱們來反推keyof在什麼狀況下能夠獲得咱們想要的VideoTypeui

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

  1. 分佈式條件類型
  2. 協變、逆變、雙變、不變。(這是一個關於在什麼狀況下才是布偶貓父類的哲學問題 -_-!!! )

不瞭解這兩個概念的小夥伴,請先看到這裏,動手去搜搜相關的文章,去夯實基礎,而後再回來繼續通關!

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']>
複製代碼

場景2、hasOwnProperty

咱們先看一下這樣的函數

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
    }
}
複製代碼

場景3、DefineProperty

當咱們定義一個對象,而後經過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 複製代碼
相關文章
相關標籤/搜索