<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          TS類型體操入門 —— 實(shí)現(xiàn)DeepKeyOf

          共 7619字,需瀏覽 16分鐘

           ·

          2022-03-01 06:41

          術(shù)??堅(jiān)??

          背景

          我們知道,在ts中提供了keyof關(guān)鍵字讓我們能夠獲取一個(gè)interface的全部的key

          interface?Stu?{
          ????name:?string;
          ????age:?number;
          }

          type?keys?=?keyof?Stu;?//?type?keys?=?'name'?|?'age'

          但并沒有提供下面這樣的遞歸地獲取一個(gè)interface的key的能力

          interface?Stu?{
          ????name:?string;
          ????age:?number;
          ????nest:?{
          ????????a:?{
          ????????????b:?number;
          ????????}
          ????}
          }

          //?expected
          type?deepKeys?=?deepkeyof?Stu;?//?type?deepKeys?=?'name'?|?'age'?|?'nest'?|?'nest.a'?|?'nest.a.b'

          可實(shí)際上這種能力非常有用,比如在類似于lodash.get的這種場景下,如果能夠有類似于deepkeyof這樣的能力,我們就可以直接將path的可能參數(shù)都提前限定下來,而不只是簡單的設(shè)置為string。

          let?obj?=?{
          ????a:?{
          ????????b:?{
          ????????????c:?'2333'
          ????????}
          ????}
          }

          _.get(obj,?'a.b.c');?//?'2333'

          實(shí)現(xiàn)思路

          1. 什么是類型體操

          類型體操(type gymnastics)一詞最早出現(xiàn)于Haskell的文檔[1]中,我認(rèn)為可以簡單理解為類型編程,和我們平時(shí)使用JavaScript、C++等語言處理各種值的編程其實(shí)沒有本質(zhì)區(qū)別,只不過在類型編程中,處理的值是類型。

          在JS中,我們在處理的值就是JS這門語言為我們提供的值(包括各種基礎(chǔ)數(shù)據(jù)類型及復(fù)雜類型),JS同時(shí)給我們提供了一系列的操作符,例如下面的 + 、* ,以及像 split 這樣的內(nèi)置的API。

          let?a?=?1;
          let?b?=?a?+?2;
          let?c?=?b?*?5;

          let?s?=?'typescript';
          let?t?=?s.split('s');

          在TypeScript的類型編程中,我們所處理的值變成了類型這一事物,查看以下代碼

          type?Temp?=?{
          ????name:?string;
          ????age:?number;
          }

          type?keys?=?keyof?Temp;?//?type?keys?=?'name'?|?'age'

          這里出現(xiàn)的值有 Temp操作符keyof 。這里使用了 keyof 來提取Temp中的key。

          可能看到這里,你對類型編程還是沒有一個(gè)具體的概念,我們做以下對比來感受一下類型編程和我們平時(shí)通常意義認(rèn)為的編程的區(qū)別。

          • 定義變量

          在類型編程中,我們定義的“變量”是不可變的,我們只能去生成新的變量,而不能去修改已有的變量,這一點(diǎn)和函數(shù)式編程語言類似。

          //?in?JavaScript
          let?a?=?1;
          const?b?=?10;
          //?in?type?gymnastics
          type?a?=?1;

          a?=?2;
          type?b?=?2;
          • 條件

          類型編程中只有表達(dá)式,沒有“語句”這一概念,也就沒有所謂的條件語句,不過有extends ? :表達(dá)式可以幫助我們達(dá)到類似的目的。

          //?in?JavaScript
          let?max;
          let?a?=?10,?b?=?20;
          if?(a?>?b)?{
          ????max?=?a;
          }?else?{
          ????max?=?b;
          }
          //?in?type?gymnastics
          type?temp?=?10;

          type?isNumber?=?T?extends?number???true?:?false;

          type?res?=?isNumber;?//?type?res?=?true;
          • 循環(huán)

          類型編程中沒有循環(huán),不過可以用遞歸來模擬。

          //?in?JavaScript
          let?index?=?1;

          let?n?=?10;

          while(n--)?{
          ????index?*=?10;
          }
          //?in?type?gymnastics
          type?path?=?'a.b.c';

          type?Split?=?T?extends?`${infer?A}.${infer?B}`???A?|?Split?:?T;

          type?test?=?Split;?//?type?test?=??a??|??b??|??c?
          • 函數(shù)

          ts的類型編程中,泛型相當(dāng)于是函數(shù),只不過泛型本身沒法當(dāng)做參數(shù)被傳遞給泛型

          function?temp(arg:?string)?{
          ????return?arg?+?'123';
          }
          type?Temp?=?`${T}123`;

          2. TS提供的部分類型操作

          • keyof
          • 索引類型[2] 獲取一個(gè)interface某個(gè)字段對應(yīng)的類型
          interface?Stu?{
          ????name:?string;
          ????age:?number;
          ????nest:?{
          ????????a:?{
          ????????????b:?number;
          ????????}
          ????}
          }

          type?temp?=?Stu['nest'];??//?type?temp?=?{?a:?{?b:?number;?};?}

          type?temp1?=?Stu['next'?|?'name']?//?type?temp1?=?string?|?{?a:?{?b:?number;?};?}
          • 模板字符串[3] 構(gòu)造各種各樣的字符串
          type?key1?=?'nest';
          type?key2?=?'a';
          type?key3?=?`${key1}.${key2}`;?//?type?key3?=??nest.a?

          type?key4?=?'name'?|?'nest';?//?union用于模板字符串也會得到一個(gè)union
          type?key5?=?`get${key4}`;?//?type?key5?=?'getname'?|?'getnest'

          //?如果模板字符串的輸入中出現(xiàn)了never,會導(dǎo)致整個(gè)字符串變成never
          type?key6?=?`get${never}`;?//?type?key6?=?never
          • extends + infer 從某個(gè)type中解構(gòu)出其組成部分
          type?UnpackPromisePromise>?=?T?extends?Promise???A?:?never;

          type?test?=?UnpackPromise<Promise>?//?type?test?=?number
          • 泛型 + 遞歸 ts類型編程的基礎(chǔ)
          type?path?=?'a.b.c';

          type?Split?=?T?extends?`${infer?A}.${infer?B}`???A?|?Split?:?T;

          type?test?=?Split;?//?type?test?=??a??|??b??|??c?
          • mapped type 基于一個(gè)已有的interface構(gòu)造一個(gè)新的interface
          interface?Temp?{
          ????name:?string;
          ????age:?number;
          }

          type?ToFunc?=?{
          ????[k?in?keyof?Temp]:?(arg:?Temp[k])?=>?void;
          }

          //?type?ToFunc?=?{
          //?????name:?(arg:?string)?=>?void;
          //?????age:?(arg:?number)?=>?void;
          //?}

          //?甚至能把原interface中的key也改了
          type?ToGetFunc?=?{
          ????[k?in?keyof?Temp?as?`get${k}`]:?(arg:?Temp[k])?=>?void;
          }

          //?type?ToGetFunc?=?{
          //?????getname:?(arg:?string)?=>?void;
          //?????getage:?(arg:?number)?=>?void;
          //?}

          3. 順序執(zhí)行方案

          回到我們最初的問題,實(shí)現(xiàn)DeepKeyOf,我們可以先嘗試使用順序執(zhí)行的方式實(shí)現(xiàn)

          1. 使用keyof拿到第一層的key
          interface?Stu?{
          ????name:?string;
          ????age:?number;
          ????nest:?{
          ????????a:?{
          ????????????b:?number;
          ????????}
          ????}
          }

          type?keys1?=?keyof?Stu;?//?type?keys1?=?'name'?|?'age'?|?'nest';
          1. 拿到第一層的key以后,用來獲取第二層的類型
          type?types2?=?Stu[keys1];?//?type?types2?=?string?|?number?|?{?a:?{?b:?number;?}?}
          1. 將第二層中仍然有多層結(jié)構(gòu)的type過濾出來
          type?OnlyObject?=?T?extends?Record???T?:?never;
          type?types2_needed?=?OnlyObject;?//?type?types2_needed?=?{?a:?{?b:?number;?}?}?
          1. 把第二層的key拿出來
          type?keys2?=?keyof?types2_needed;?//?type?keys2?=??a?
          1. 使用步驟4拿到的key重復(fù)步驟2
          1. 最終,我們可以拿到每一層的key
          type?keys1?=?'name'?|?'age'?|?'nest';
          type?keys2?=?'a';
          type?keys3?=?'b';
          1. 嘗試一番后發(fā)現(xiàn)使用這樣順序執(zhí)行的方式似乎是沒法做到把不同層之間的key正確連接起來的,比如keys2中的'a'并不知道自己是由keys1中哪個(gè)key對應(yīng)的type中獲取的。

          4. 遞歸方案

          1. 雖然順序執(zhí)行的方式不能解決問題,但是可以為我們的遞歸的解決方案提供思路。我們的輸入是一個(gè)任意的interface,輸出是它的各級的key連接以后的一個(gè)union。
          //?input
          interface?Stu?{
          ??name:?string;
          ??nest:?{
          ????a:?{
          ??????b:?number;
          ????};
          ????tt:?{
          ??????c:?boolean;
          ????};
          ??};
          ??info:?{
          ????score:?number;
          ????grade:?string;
          ??};
          }

          //?output
          'name'?|?'nest'?|?'nest.a'?|?'nest.a.b'?|?'nest.tt'?|?'nest.tt.c'?|?'info'?|'info.score'?|?'info.grade'

          在順序執(zhí)行步驟的第五步中,我們需要重復(fù)第二步,可以猜測我們現(xiàn)在需要解決的這個(gè)問題是可以分解為同類的子問題的。

          仔細(xì)觀察,可以發(fā)現(xiàn)我們的問題可以這樣來表示

          1. 假設(shè)我們有一個(gè)泛型DeepKeyOf可以獲取我們需要的key
          type?DeepKeyOf?=?xxx;
          type?test?=?DeepKeyOf?//?type?test?=?'name'?|?'nest'?|?'nest.a'?|?'nest.a.b'?|?'nest.tt'?|?'nest.tt.c'?|?'info'?|'info.score'?|?'info.grade'
          1. 其內(nèi)部實(shí)現(xiàn)大概會是下面這樣
          type?DeepKeyOf?=?{
          ????[k?in?keyof?T]:?k;
          }[keyof?T];

          type?DeepKeyOf?=?{
          ????[k?in?keyof?T]:?k?|??`??${k}??.??${DeepKeyOf}??`??;?
          }[keyof?T]

          這一步可能比較跳,我們簡單解釋一下。

          • 粉色部分用到的是前面有講到過的mapped type,它基于傳入DeepKeyOfT來構(gòu)造一個(gè)新的interface,它的key和T一致,value變成了k`${k}.${DeepKeyOf}` 的union
          • 黃色部分用到的是上文提到過的index access type,它取出粉色部分對應(yīng)的interface的全部字段對應(yīng)的類型
          1. 我們再將 Stu 類型傳入,手動把這個(gè)泛型展開
          interface?Stu?{
          ??name:?string;
          ??nest:?{
          ????a:?{
          ??????b:?number;
          ????};
          ????tt:?{
          ??????c:?boolean;
          ????};
          ??};
          ??info:?{
          ????score:?number;
          ????grade:?string;
          ??};
          }

          type?res?=?DeepKeyOf;
          type?res?=?{
          ????name:?'name'?|?`${'name'}.${DeepKeyOf}`;
          ????nest:?'nest'?|?`${'nest'}.${DeppKeyOf<{a:?{b:?number;}?tt:?{c:?boolean;}}>}`;
          ????info:?'info'?|?`${'info'}.${DeepKeyOf<{score:?number;?grade:?string;}>}`;
          }['name'?|?'nest'?|?'info'];
          type?res?=?'name'?|?`${'name'}.${DeepKeyOf}`?|?'nest'?|?`${'nest'}.${DeppKeyOf<{a:?{b:?number;}?tt:?{c:?boolean;}}>}`?|?'info'?|?`${'info'}.${DeepKeyOf<{score:?number;?grade:?string;}>}`;

          如果我們的DeepKeyOf實(shí)現(xiàn)正確的話,DeepKeyOf 應(yīng)當(dāng)返回neverDeppKeyOf<{a: {b: number;} tt: {c: boolean;}}>應(yīng)當(dāng)返回 'a' | 'a.b' ,DeepKeyOf<{score: number; grade: string;}> 應(yīng)當(dāng)返回 'score' | 'grade'

          never是union “|” 這一運(yùn)算中的幺元[4],任何type與never做union運(yùn)算均為其本身

          如:'name' | 'nest' | never === 'name' | 'nest'

          1. 補(bǔ)充細(xì)節(jié)
          • 對于非原始類型,我們應(yīng)該直接返回never,如DeepKeyOf 應(yīng)當(dāng)返回never,我們修改DeepKeyOf實(shí)現(xiàn)如下
          type?DeepKeyOf?=?T?extends?Record???{
          ????[k?in?keyof?T]:?k?|?`?${k}?.?${DeepKeyOf}?`?;
          }[keyof?T]?:?never;
          • 上面的代碼實(shí)際上放到IDE里,會提示我們這樣一個(gè)錯(cuò)誤

          這是因?yàn)?code style="font-size: 14px;word-wrap: break-word;border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #9b6e23;background-color: #fff5e3;padding: 3px;margin: 3px;">keyof T在沒有對T做類型限制的情況下拿到的類型會是string | number | symbol,而模板字符串可以接收的類型是 string | number | bigint | boolean | null | undefined,這兩個(gè)type之間是不兼容的,我們需要對k的類型做進(jìn)一步的限制

          type?DeepKeyOf?=?T?extends?Record???{
          ????[k?in?keyof?T]:?k?extends?string???k?|?`?${k}?.?${DeepKeyOf}?`??:?never?;
          }[keyof?T]?:?never;
          5.最終我們就得到了我們需要的DeepKeyOf泛型
          type?DeepKeyOf?=?T?extends?Record<string,?any>???{
          ????[k?in?keyof?T]:?k?extends?string???k?|?`${k}.${DeepKeyOf}`?:?never;
          }[keyof?T]?:?never;

          interface?Stu?{
          ??name:?string;
          ??nest:?{
          ????a:?{
          ??????b:?number;
          ????};
          ????tt:?{
          ??????c:?boolean;
          ????};
          ??};
          ??info:?{
          ????score:?number;
          ????grade:?string;
          ??};
          }

          type?res?=?DeepKeyOf;?//?"name"?|?"nest"?|?"info"?|?"nest.a"?|?"nest.tt"?|?"nest.a.b"?|?"nest.tt.c"?|?"info.score"?|?"info.grade"

          總結(jié)

          以上,我們通過實(shí)現(xiàn)一個(gè)簡單的DeepKeyOf了解了類型編程這一概念,它和我們通常意義認(rèn)為的編程其實(shí)沒有本質(zhì)區(qū)別,只是編程中操作的值不同,使用的運(yùn)算符不同。在實(shí)際業(yè)務(wù)開發(fā)過程中,利用類型編程的知識,可以把一些類型收窄到我們實(shí)際需要的范圍,比如需要一個(gè)整數(shù)形式的字符串,我們完全可以使用`${bigint}`,而不是簡單的使用一個(gè)string。收窄到實(shí)際需要的范圍不僅可以提高開發(fā)體驗(yàn)(類型收窄后,IDE可以相應(yīng)地提供智能提示,也可以避免大量類型模板代碼),也能提高代碼的可讀性(比如一個(gè)函數(shù)的輸入輸出可以更加明確)。

          ps:如果你對類型編程感興趣,可以嘗試一下這個(gè)體操庫[5],這個(gè)repo里有比較多有用的體操題,并且提供了難度分級,同時(shí)可以查看其他人的solution,幫助我們快速上手類型體操。

          參考資料

          [1]

          Haskell的文檔: https://wiki.haskell.org/index.php?title=OOP_vs_type_classes&oldid=5437#Type_classes_is_a_sort_of_templates.2C_not_classes

          [2]

          索引類型: https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

          [3]

          模板字符串: https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

          [4]

          幺元: https://zh.wikipedia.org/zh-cn/%E5%96%AE%E4%BD%8D%E5%85%83

          [5]

          這個(gè)體操庫: https://github.com/type-challenges/type-challenges/blob/master/README.md

          ???

          便內(nèi),^_^

          ?點(diǎn)、?~

          關(guān)?前端Sharing?收獲~


          瀏覽 85
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    午夜精品一区二区三区在线播放 | 爆乳一区二区 | 97超碰囯产 | 欧美毛片一区二区三区有限公司 | 久久激情亚洲色 |