TS 型別體操:圖解一個複雜高階型別

語言: CN / TW / HK

今天就來做個高難度的體操,它會綜合運用模式匹配、構造、遞迴等套路,對提升型別程式設計水平很有幫助。

我們要實現的高階型別如下:

它的型別引數是引數字串 query string,會返回解析出的引數物件,如果有同名的引數,會把值做合併。

先不著急實現,我們先回顧下相關的型別體操基礎:

型別體操基礎

模式匹配

模式匹配是指用一個型別匹配一個模式型別來提取其中的部分型別到 infer 宣告的區域性變數中。

比如提取 a=b 中的 a 和 b:

這種模式匹配的套路在陣列、字串、函式等型別中都有很多應用。

構造

對映型別用於生成索引型別,生成的過程中可以對索引或者索引值做一些修改。

比如指定 key 和 value 來生成一個索引型別:

遞迴

TypeScript 高階型別支援遞迴,可以處理數量不確定的問題。

比如不確定長度的字串的反轉:

type ReverseStr< 
    Str extends string,
    Result extends string = '' 
> = Str extends `${infer First}${infer Rest}` 
    ? ReverseStr<Rest, `${First}${Result}`> 
    : Result;
複製程式碼

簡單瞭解下模式匹配、構造、遞迴都是什麼之後,就可以開始實現這個複雜的高階型別 ParseQueryString 了:

思路分析

假設有這樣一個 query string: a=1&a=2&b=3&c=4

我們要首先把它分成 4 部分:也就是 a=1、a=2、b=3、c=4。這個就是用通過上面講的模式匹配來提取。

每一部分又可以進一步處理,提取出 key value 構造成索引型別,比如 a=1 就可以通過模式匹配提取出 a、1,然後構造成索引型別 {a: 1}。

這樣就有了 4 個索引型別 {a:1}、{a:2}、{b:3}、{c:4}。

結下來把它合併成一個就可以了,合併的時候如果有相同的 key 的值,要放到數組裡。

就產生了最終的索引型別:{a: [1,2], b: 3, c: 4}

整體流程是這樣的:

其中第一步並不知道有多少個 a=1、b=2 這種 query param,所以要遞迴的做模式匹配來提取。

這就是這個高階型別的實現思路。

下面我們具體來寫一下:

程式碼實現

我們按照上圖的順序來實現,首先提取 query string 中的每一個 query param:

query param 數量不確定,所以要用遞迴:

type ParseQueryString<Str extends string>
    = Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>> 
        : ParseParam<Str>;
複製程式碼

型別引數 Str 為待處理的 query string。

通過模式匹配提取其中第一個 query param 到 infer 宣告的區域性變數 Param 中,剩餘的字串放到 Rest 中。

用 ParseParam 來處理 Param,剩餘的遞迴處理,最後把它們合併到一起,也就是 MergeParams<ParseParam<Param>, ParseQueryString> 。

如果模式匹配不滿足,說明還剩下最後一個 query param 了,也用 ParseParam 處理。

然後分別實現每一個 query param 的 parse:

這個就是用模式匹配提取 key 和 value,然後構造一個索引型別:

type ParseParam<Param extends string> 
    = Param extends `${infer Key}=${infer Value}` 
        ? { [K in Key]: Value } 
        : {};
複製程式碼

這裡構造索引型別用的就是對映型別的語法。

先來測試下這個 ParseParam:

做完每一個 query param 的解析了,之後把它們合併到一起就行:

合併的部分就是 MergeParams:

type MergeParams<
    OneParam extends object,
    OtherParam extends object
> = {
  [Key in keyof OneParam | keyof OtherParam]: 
    Key extends keyof OneParam
        ? Key extends keyof OtherParam
            ? MergeValues<OneParam[Key], OtherParam[Key]>
            : OneParam[Key]
        : Key extends keyof OtherParam 
            ? OtherParam[Key] 
            : never
}
複製程式碼

兩個索引型別的合併也是要用對映型別的語法構造一個新的索引型別。

key 是取自兩者也就是 key in keyof OneParam | keyof OtherParam。

value 要分兩種情況:

  • 如果兩個索引型別都有的 key,就要做合併,也就是 MergeValues。
  • 如果只有其中一個索引型別有,那就取它的值,也就是 OtherParam[key] 或者 OneParam[Key]。

合併的時候,如果兩者一樣就返回任意一個,如果不一樣,就合併到數組裡返回,也就是 [One, Other]。如果本來是陣列的話,那就是陣列的合併 [One, ...Other]。

type MergeValues<One, Other> = 
    One extends Other 
        ? One
        : Other extends unknown[]
            ? [One, ...Other]
            : [One, Other];
複製程式碼

測試下 MergeValues:

這樣,我們就實現了整個高階型別,整體測試下:

這個案例綜合運用到了遞迴、模式匹配、構造的套路,還是比較複雜的。

可以對照著這張圖來看下完整程式碼:

type ParseParam<Param extends string> = 
    Param extends `${infer Key}=${infer Value}`
        ? {
            [K in Key]: Value 
        } : {};

type MergeValues<One, Other> = 
    One extends Other 
        ? One
        : Other extends unknown[]
            ? [One, ...Other]
            : [One, Other];

type MergeParams<
    OneParam extends object,
    OtherParam extends object
> = {
  [Key in keyof OneParam | keyof OtherParam]: 
    Key extends keyof OneParam
        ? Key extends keyof OtherParam
            ? MergeValues<OneParam[Key], OtherParam[Key]>
            : OneParam[Key]
        : Key extends keyof OtherParam 
            ? OtherParam[Key] 
            : never
}

type ParseQueryString<Str extends string> = 
    Str extends `${infer Param}&${infer Rest}`
        ? MergeParams<ParseParam<Param>, ParseQueryString<Rest>>
        : ParseParam<Str>;


type ParseQueryStringResult = ParseQueryString<'a=1&a=2&b=2&c=3'>;
複製程式碼

總結

我們首先複習了下 3 種類型體操的套路:

  • 模式匹配:一個型別匹配一個模式型別,提取其中的部分型別到 infer 宣告的區域性變數中
  • 構造:通過對映型別的語法來構造新的索引型別,構造過程中可以對索引和值做一些修改
  • 遞迴:當處理數量不確定的型別時,可以每次只處理一個,剩下的遞迴來做

然後用這些套路來實現了一個 ParseQueryString 的複雜高階型別。

如果能獨立實現這個高階型別,說明你對這三種類型體操的套路掌握的就挺不錯的了。

最後

如果你覺得此文對你有一丁點幫助,點個贊。或者可以加入我的開發交流群:1025263163相互學習,我們會有專業的技術答疑解惑

如果你覺得這篇文章對你有點用的話,麻煩請給我們的開源專案點點star:http://github.crmeb.net/u/defu不勝感激 !

PHP學習手冊:http://doc.crmeb.com
技術交流論壇:http://q.crmeb.com