<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】1708- 拋磚引玉:TypeScript 從入門到實踐

          共 27628字,需瀏覽 56分鐘

           ·

          2023-06-10 00:13

          介紹

          眾所周知 JavaScript 是一門弱類型語言,在前端進入工程化后,代碼倉庫越來越大,JavaScript 弱類型的缺點被無限放大,使其難以勝任開發(fā)大型項目。在一個多人維護的項目中,往往不知道別人寫的函數(shù)是什么意思、需要傳入什么參數(shù)、返回值是什么,一個用法不小心就會導(dǎo)致線上出現(xiàn) BUG,所以除了靠口口相傳以外還要維護大量的代碼注釋或者接口文檔來提供其他人了解。但是當(dāng)我們使用 TypeScript 后,除了初期具有一定的學(xué)習(xí)成本以外,基本上可以很好的解決上述的問題。

          TypeScriptJavaScript 的嚴格超集,這意味著任何合法的 JavaScript 代碼在 TypeScript 中都是合法的。TS 的作者是 安德斯·海爾斯伯格[1],2012 年 10 月,微軟發(fā)布了首個公開版本的 TypeScript,2013 年 6 月 19 日正式發(fā)布了正式版 TypeScript。根據(jù) Roadmap[2] 我們可以知道 TS 官方每隔三個月會更新一個正式的 minor 版本,比如會在今年的 5.27 發(fā)布了 v4.7[3]。更多關(guān)于 TS 的故事可以查看 TypeScript 團隊成員 orta 的文章:Understanding TypeScript's Popularity[4]

          當(dāng)然在 TS 徹底大火之前,同期也存在很多相似的工具來輔助開發(fā)者做好類型提示,比如 Flow[5] 、JSDoc[6] 等。

          ef9f40e8dcce84c3e5f17a77e33f00f1.webp

          JSDoc,通過注釋的方式給 add 函數(shù)添加類參數(shù)類型和返回值的類型。通過在編輯器頂部添加 @ts-check 的注釋開始 VSCode 對其的檢查。

          7ff9259aeb0466f0197d37aac90c48a7.webp

          Flow,類似 TS 的寫法添加類型注解,可以通過安裝插件或者命令掃描出當(dāng)前代碼中有問題的地方。

          根據(jù) npm 的現(xiàn)在趨勢,目前 Flow 的使用率比 TS 低了很多。

          0b2c85ca83956eeec5e9a7fc25e665dc.webp

          經(jīng)過眾多的開發(fā)者選擇,目前來看 TS已經(jīng)是完全勝出了。許多著名的開源庫都采用 TS 進行編寫。比如 VueJS,其中 v2.x 版本是使用的 Flow 進行的類型編寫,但是 v3.x 版本已經(jīng)全部遷移到了 TS。至于為什么選擇 TS 替代 Flow 可以參看 尤玉溪的回答[7] 。

          所以說 TS 逐漸的得到了社區(qū)的認可,就像 HTMLCSS 、 JS 一樣快成為一名前端開發(fā)者必備的技能。所以我們不得不去學(xué)習(xí)它、并將它靈活的運用在項目當(dāng)中。

          基礎(chǔ)使用
          1. 基本類型的介紹與適用場景
          2. 交叉和聯(lián)合類型

          3. 類型檢查機制:推斷、斷言、保護和守衛(wèi)

          4. 全局類型、類型引入、類型重寫、類型合并(interface 和 type 的區(qū)別)

          基礎(chǔ)類型

          由于 TSJS 的嚴格超集,所以 JS 中支持的類型在 TS 中肯定支持,所以在 JS 代碼中使用什么類型的變量,在 TS 中也使用該類型。

          TS 具有類型自動推斷的能力,比如當(dāng)我們使用 const 或者 let 聲明一個變量的時,如果直接有賦值,那么就會給當(dāng)前變量設(shè)置成這個賦值的類型。如下:

                
                let?str?=?'hello';?//?let?str:?string;
          let?num?=?666;??//?let?num:?number;
          let?bool?=?true;?//?let?bool:?boolean
          let?undef?=?undefined;?//?let?undef:?undefined
          let?nul?=?null;?//?let?nul:?null
          let?sym?=?Symbol(123);?//?let?sym:?symbol
          const?fn?=?()?=>?123;?//?const?fn:?()?=>?number
          let?res?=?fn();?//?let?res:?number
          let?arr?=?[1,?2,?'abc'];?//?let?a:?(string?|?number)[]
          const?obj?=?{
          ????a:?1,
          ????b:?true,
          }
          /*?const?obj:?{
          ????a:?number;
          ????b:?boolean;
          }*/

          在有些同學(xué)的嘗試過程中,當(dāng)賦值為 undefinednull 時,會自動推斷成 any 。這是由于 tsconfig.json 中的配置有問題,把 compilerOptions.strictNullChecks 設(shè)置為 true 即可。目前發(fā)現(xiàn)團隊中有部分項目都沒有開啟該配置,那么可能會在運行時出現(xiàn)意料之外的 BUG

          新增的類型

          當(dāng)然為了代碼的靈活編寫 TS 還具有一些獨特的類型:

          196a0198675bb1bf1505c9db3974c1e0.webp

          類型間的關(guān)系

          通過上文我們可以得出類型之間的關(guān)系:

          1d8265aa2c0c5e8870a211c515ca4208.webp

          以上表格是在嚴格模式下,即 strictNullChecks true 也是推薦的配置

          聯(lián)合與交叉

          我們在實際的開發(fā)過程中變量的類型并不是單一的,比如 array.find 既可以返回數(shù)據(jù)的類型也可以返回 undefined,或者說我們寫一個 mixin 的方法需要將 2 個類型合并。

          交叉類型

          交叉類型是使用 & 符合將多個類型合并成一個類型。如:type C = A & B 這樣 C 類型 就既有 A 的類型 也有 B 的類型。常見的如 Object.assign 方法,可以將對象進行合并,所以就需要這樣的方法將每個對象的類型進行合并,或者說我們在編寫 React 高階組件時,編寫的過程中就可以對已有類型進行拓展。

                
                interface?Props?{
          ??name:?string;
          ??age:?number;
          }
          interface?WithHOCProps?{
          ??options:?{
          ????size:?number;
          ??}
          }

          const?App:?React.FC?=?({
          ??options,
          })?=>?{
          ??options.size?//?number
          }

          由于是類型合并,所以可能會遇到 2 個類型不兼容的情況,所以如果遇到不兼容的類型就會推導(dǎo)出 never

                
                interface?Item1?{
          ??id:?string;
          ??name:?string;
          }
          interface?Item2?{
          ??id:?number;
          ??age:?number;
          }

          type?C?=?Item1?&?Item2;
          type?Id?=?C['id']?//?never
          type?Name?=?C['name']?//?string
          type?Age?=?C['age']?//?number

          因為上文(類型間的關(guān)系)已經(jīng)展示出了不一樣的類型也存在可以相互轉(zhuǎn)換的。所以 A & B的運算關(guān)系可以看成:

          • A 和 B 可以相互賦值 => 目前只有 any 可以滿足這種情況
          • A 可以賦值給 B,B 不能賦值給 A => A

          • B 可以賦值給 A,A 不能賦值給 A => B

          • A 和 B 不存在可以賦值的關(guān)系 => never

                
                type?T1?=?number?&?string;?//?never
          type?T2?=?number?&?unknown;?//?number
          type?T3?=?number?&?any;?//?any
          type?T4?=?number?&?never;?//?never

          聯(lián)合類型

          聯(lián)合類似是使用 | 符合將多個類型聯(lián)系起來,如:C = A | B 表明 C 要么等于類型 A 要么等于類型 B。主要用于當(dāng)我們一個變量的類型不固定時,比如一個函數(shù)運行過程中正常的運算結(jié)果返回 A ,運算失敗返回 B

                
                const?fn?=?(num:?number):?number?|?string?=>?{
          ????if?(num?>=0)?{
          ????????return?num;
          ????}?else?{
          ????????return?'error';
          ????}
          }
          const?res?=?fn(1);

          當(dāng)一個值是聯(lián)合類型時,只可以調(diào)用聯(lián)合類型的共有屬性。如上面的 res 類型是 number | string ,如果不加以判斷只能調(diào)用共有的 toStringvalueOf 等方法。

          類型推斷與保護

          正常情況下類型具有自動推斷的能力,比如我們聲明一個變量 const num = 1,TS 會自動將變量的類型推斷成 number,所以后面我們就可以對 num 變量使用 number 的一些操作方法。但是當(dāng)使用聯(lián)合類型的時候,TS 在編譯階段就無法得知當(dāng)前的變量類型是什么,所以只可以使用共有的一些方法,所以我們需要使用類型保護的能力,比如可以通過一些判斷來縮小當(dāng)前變量或者斷言當(dāng)前變量的類型。

          typeof

          typeof 是判斷變量類型的一個操作符,我們可以通過typeof將類型縮小變成一個受保護的類型,如:

                
                const?fn?=?(value:?number?|?string):?void?=>?{
          ??value?//?value?is?number?|?string

          ??if?(typeof?value?===?'number')?{
          ????value?//?value?is?number
          ????value.toFixed?//?no?error
          ??}?else?{
          ????value?//?value?is?string
          ????value.length?//?no?error
          ??}
          }

          instanceof

          typeof 類似,instanceof 也是一個判斷變量類型的方式,比如一個函數(shù)可以同時接收不同的普通的對象,就會導(dǎo)致 typeof value === 'object' 分辨不出來。就可以使用 instanceof 來辨別對象是什么。

                
                class?User?{
          ??say():?void?{
          ????console.log('hello');
          ??}
          }

          class?Stu?{
          ??read():?void?{
          ????console.log('read');
          ??}
          }

          const?fn?=?(value:?Stu?|?User):?void?=>?{
          ??value?//?value?is?Stu?|?User

          ??if?(value?instanceof?User)?{
          ????value?//?value?is?User
          ????value.say()?//?no?error
          ??}?else?{
          ????value?//?value?is?Stu
          ????value.read?//?no?error
          ??}
          }

          in

          in 可以檢查是否存在某個屬性,如:

                
                type?Item1?=?{
          ??type:?'item1';
          ??name:?string;
          ??age:?number;
          }

          type?Item2?=?{
          ??type:?'item2',
          ??title:?string;
          ??description:?string;
          }

          const?fn?=?(value:?Item1?|?Item2):?void?=>?{
          ??if?('name'?in?value)?{
          ????value.age?//?no?error
          ??}?else?{
          ????value.description?//?no?error
          ??}
          }

          字面量判斷

          如上面的例子,如果后期對 Item2添加name屬性就容易導(dǎo)致這里類型判斷失效,導(dǎo)致獲取 value.age 就會存在問題。所以針對上述這種存在 type 區(qū)分的情況,可以直接使用字面量判斷。

                
                const?fn?=?(value:?Item1?|?Item2):?void?=>?{
          ??if?(value.type?===?'item1')?{
          ????value.age?//?no?error
          ??}?else?if?(value.type?===?'item2')?{
          ????value.description?//?no?error
          ??}
          }

          is 關(guān)鍵字自定義

          使用上面的方法在簡單的場景下是十分有效,但是有時候類型的判斷是復(fù)雜的,或者這樣的判斷是通用的,所以為了避免重復(fù)的編寫我們可能需要對這個類型的保護需要提取成函數(shù),那么就可以使用 is 來進行指定。

                
                type?Item1?=?{
          ??type:?'item1';
          ??name:?string;
          ??age:?number;
          }

          type?Item2?=?{
          ??type:?'item2',
          ??title:?string;
          ??description:?string;
          }

          const?isItem1Arr?=?(value:?any):?value?is?Item1[]?=>?{
          ??if?(!Array.isArray(value))?{
          ????return?false;
          ??}
          ??if?(value.length?===?0)?{
          ????return?true;
          ??}
          ??return?value.every(item?=>?item.type?===?'item1');
          }

          const?fn?=?(value:?Item1[]?|?Item2[]):?void?=>?{
          ??if?(isItem1Arr(value))?{
          ????value.forEach(item?=>?{
          ??????item.age?//?no?error
          ????});
          ??}
          }

          其中 isItem1Arr 返回值是一個 boolean 如果返回的是 true 則說明當(dāng)前類型是 is 后面的。

          類型斷言

          有了類型斷言我們可以輕松的遷移一個項目,但是類型斷言是有害的,因為我們主動的給這個變量向 TS 類型檢查器做了背書而不是通過類型保護的方式。所以假設(shè)傳入的數(shù)據(jù)是有誤的,就會導(dǎo)致運行異常,所以我們需要謹慎的使用類型斷言,除非可以 100% 的保證這里類型。

          as 與 <>

          有的時候 TS 的檢驗規(guī)則是存在缺陷的,不能完美的做好類型保護,比如下面的例子,雖然我們已經(jīng)提前判斷過 item.parent 肯定不為空,但是在一個閉包環(huán)境中使用,由于 TS 的缺陷,類型還是失效了(因為我們是馬上運行的)。但是我們可以保證這里的類型肯定是不會為 null 的,所以我們就可以斷言它的類型。

                
                interface?Item?{
          ??parent:?Item?|?null;
          }

          const?fn?=?(item:?Item)?=>?{
          ??if?(!item.parent)?{
          ????return;
          ??}
          ??const?_fn?=?()?=>?{
          ????item.parent?//?Item?|?null
          ????const?parent1?=?item.parent?as?Item;
          ????const?parent2?=?item.parent;
          ??}
          ??_fn();
          }

          第二種情況是,這個值是在運行過程中產(chǎn)生。所以我們沒有辦法在定義變量的時候進行初始化,這個時候就需要使用類型斷言了。但是這種方式存在弊端,假設(shè)后面 User 新增了一個屬性,就會導(dǎo)致返回的數(shù)據(jù)有缺省。所以不是很推薦這種方式,而是可以在初始化的時候設(shè)置成空值/默認值,這樣當(dāng)新增一個屬性后,就會主動報錯,提醒我們需要處理額外的屬性。

                
                interface?User?{
          ??type:?'student';
          ??name:?string;
          }

          const?createUser?=?(name:?string):?User?=>?{
          ??//?const?result?=?{
          ??//???type:?'student'
          ??//?}?as?User;
          ??const?result?=?{
          ????type:?'student'
          ??};

          ??if?(name)?{
          ????result.name?=?parseName(name);
          ??}

          ??return?result;
          }

          ! 非空斷言

          顧名思義,主要是排除變量中 nullundefined 的類型。比如上面提到的 item.parent 我們可以很清楚的知道他不是一個 null 的,就可以使用這個簡單的方式。

                
                interface?Item?{
          ??parent:?Item?|?null;
          }

          const?fn?=?(item:?Item)?=>?{
          ??if?(!item.parent)?{
          ????return;
          ??}
          ??const?_fn?=?()?=>?{
          ????const?p1?=?item.parent.parent;?//?error:?(item.parent)對象可能為?null
          ????const?p2?=?(item.parent).parent?//?ok,item.parent?整體斷言
          ????const?p3?=item.parent!.parent;?//?ok,item.parent?非空,則排除?null
          ??}
          }

          雙重斷言

          毫無根據(jù)的斷言是危險的,所以進行類型斷言時,TS 會提供額外的安全性,并不是每個變量間都可以斷言的,比如 'licy' as number 將一個字符串轉(zhuǎn)換成 number 肯定就是不行的。

          如果 AB 直接存在賦值關(guān)系,即 AB 的子類型,或者 BA 的子類型就可以直接使用斷言。如果不存在時,可以找一個中間的類型來做橋梁,通過上面【類型間的關(guān)系】可以得出 any 、unknown 、never 三個類型是最少都滿足上面 2 個規(guī)律之一。

                
                const?n1?=?'licy'?as?number;?//?error:?string?與?number?不能充分重疊,轉(zhuǎn)換是錯誤的
          const?n2?=?'aa'?as?any?as?number;
          const?n3?=?'aa'?as?unknown?as?number;?//?推薦
          const?n4?=?'aa'?as?never?as?number;

          因為斷言是具有危害性的,所以雙重斷言也是具有危害性的。我們需要盡量的少用。同時雙重斷言的使用場景很少很少,筆者只在一次跨 npm 包調(diào)用的時候,由于底層版本不一致,使用過一次。

          全局類型

          通常情況下,定義的類型需要使用 export 進行導(dǎo)出,在使用的地方再使用 import 導(dǎo)入。但是有時候在同一個項目中會有一些通用的類型或者類型方法,每次都進行導(dǎo)入是很繁瑣的。甚至需要在 window 掛載一些新的變量,所以我們需要了解全局類型的概念。聲明全局類型的方式有 2 種:

          1.              在一個 `.d.ts` 文件中寫變量類型,同時不要有 `export` 和 `import` 等導(dǎo)入導(dǎo)出語法。
                      
                
                //?global.d.ts
          type?AnyFunction?=?(...args:?any[])?=>?any;

          值得注意的是,你需要在 tsconfig.jsoninclude 選項中包含該文件。另外需要注意的一點是,如果你是一個 npm 包的類型中,如果引入 npm 包的沒有引入你定義的全局類型,則會變成any。

          1.              使用 `declare` 定義,比如需要給 `window` 新增類型,給某個包或者某一類文件添加類型說明等。
                      
                
                //?global.d.ts
          declare?module?'react'?{
          ??export?const?licy:?string;
          }

          declare?module?'npm-package'?{
          ??export?const?props:?{?name:?'licy';?age:?number?}
          ??const?App:?React.FC<typeof?props>;
          ??export?default?App;
          }

          declare?module?'*.svg'?{
          ??const?content:?{
          ????id:?string;
          ??}
          ??export?default?content;
          }


          //?app.ts
          import?React?from?'react';
          import?svg?from?'./log.svg';
          import?{?props?}?from?'npm-package';

          React.licy?//?string
          svg.id?//?string
          props.name?//?'licy'

          當(dāng)然很多時候,我們的類型還會引入一些已有類型進行組裝,所以就會破壞掉默認 .d.ts 是全局類型的約束,所以需要主動的導(dǎo)出。

                
                //?global.d.ts
          import?{?ValueOf?}?from?"./type";

          declare?namespace?CommonNS?{
          ??interface?Props?{
          ????name:?'licy';
          ????age:?24
          ??}
          ??type?Value?=?ValueOf;
          }

          //?缺一不可,否則類型使用會加前綴
          //?將?CommonNS?作為全局類型,類似?UMD
          export?as?namespace?CommonNS;
          //?將導(dǎo)出命名修改,否則就會使用?CommonNS.CommonNS.XXX?才可以獲取
          export?=?CommonNS;


          //?main.ts
          const?value:?CommonNS.Value?=?'licy';

          在全局選項這里需要注意 skipLibCheck 的配置,如果該選項配置為 true 則會跳過庫文件的類型檢查,比如 node_moduels 中其他庫的類型檢查和當(dāng)前項目的 .d.ts 檢查。所以會導(dǎo)致在編寫 .d.ts 文件的時候不一察覺錯誤,但是有不能保證引入的 npm 庫的類型文件都是正確的。所以可以在 tsconfig.json 將該選項設(shè)置為 false 然后在編譯階段再將該選項設(shè)置為 true

          高級用法

          函數(shù)重載

          函數(shù)重載是靜態(tài)類型語言當(dāng)中很重要的一個能力。很多時候編寫的函數(shù)可能會兼容多種參數(shù)類型,可能會根據(jù)傳入的參數(shù)會返回不同的數(shù)據(jù)。比如:

                
                const?data?=?{?name:?'licy'?};
          const?getData?=?(stringify:?boolean?=?false):?string?|?object?=>?{
          ??if?(stringify?===?true)?{
          ????return?JSON.stringify(data);
          ??}?else?{
          ????return?data;
          ??}
          }

          const?res1?=?getData();?//?string?|?object
          const?res2?=?getData(true);?//?string?|?object

          在上述的例子中調(diào)用 getData 方法得到一個聯(lián)合了聯(lián)合類型,還需要進行判斷將類型縮小或者使用 as 進行指定。但是如果作為方法的編寫者,當(dāng)確定傳入的參數(shù)后就可以很準確的得到返回值的類型,而不是得到這種模棱兩可的情況。所以借助函數(shù)重載進行改造:

                
                const?data?=?{?name:?'licy'?};
          function?getData(stringify:?true):?string
          function?getData(stringify?:?false):?object
          function?getData(stringify:?boolean?=?false):?unknown?{
          ??if?(stringify?===?true)?
          {
          ????return?JSON.stringify(data)
          ;
          ??}?else?{
          ????return?data;
          ??}
          }

          const?res1?=?getData();?//?object
          const?res2?=?getData(true);?//?string

          函數(shù)重載的使用方法很簡單,就是在需要使用函數(shù)重載的地方,多聲明幾個函數(shù)的類型。然后在最后一個函數(shù)中進行實現(xiàn),特別要注意的是,最后實現(xiàn)函數(shù)中的類型一定要與上面的類型兼容。

          值得注意的是由于 TS 是在編譯后會將類型抹去生成 JS 代碼,而 JS 是沒有函數(shù)重載這樣的能力,所以說這里的函數(shù)重載只是類型的重載,方便做類型的提示,實際上還是要在實現(xiàn)函數(shù)中進行傳入?yún)?shù)的判別,然后返回不同的結(jié)果。

          泛型

          泛型是 TS 一個比較高級的用法,在日常的開發(fā)中也是使用比較多的。當(dāng)你的函數(shù),接口或者類需要支持多種類型的時候就可以使用泛型,比如上面函數(shù)重載的例子,也可以使用泛型進行改造。

                
                //?泛型函數(shù)
          const?data?=?{?name:?'licy'?};
          function?getData<T?extends?boolean?=?false,?R?=?T?extends?true???string?:?object>(stringify?:?T):?R?{
          ??if?(stringify?===?true)?{
          ????return?JSON.stringify(data)?as?unknown?as?R;
          ??}?else?{
          ????return?data?as?unknown?as?R;
          ??}
          }

          const?res1?=?getData();?//?object
          const?res2?=?getData(true);?//?string

          //?泛型類型
          type?ValueOf?=?T[keyof?T];

          interface?User?{
          ??name:?'licy';
          }

          type?A?=??keyof?User;?//?'name'
          type?B?=?ValueOf;?//?'licy'

          亦或者需要對傳入進來的參數(shù)進行保存時,比如編寫 React 中的 HOC

                
                type?AnyObject?=?Record<string,?any>;
          type?ExtraProps?=?{
          ??name:?'licy'
          };
          const?withItem?=?<
          ??T?extends?AnyObject
          >(Comp:?React.FC):?React.FC?=>?{
          ??const?NewComp:?React.FC?=?(props)?=>?{
          ????props.name?//?'licy'
          ????return?
          ??}
          ??NewComp.displayName?=?'with-item';
          ??return?NewComp;
          }

          const?Demo:?React.FC<{?age:?number?}>?=?()?=>?null;

          const?NewDemo?=?withItem(Demo);

          const?res?=?(
          ??<>
          ????24}?/>?no?error
          ????24}?name="licy"?/>?no?error
          ????24}?/>?error:?缺少屬性?"name"
          ??</>
          )

          內(nèi)置的高級函數(shù)

          為了方便類型編寫,TS 官方內(nèi)置了許多通用的高級類型方法,這些方法可以幫助我們完成程序中大部分的類型轉(zhuǎn)換。但是如果我們掌握了這些類型方法的實現(xiàn)方式,也可以很輕松的寫出符合業(yè)務(wù)邏輯規(guī)范的高級方法。所有的內(nèi)置方法可以參考:utility-types[8] 本文只介紹一些典型的。

          196a0198675bb1bf1505c9db3974c1e0.webp

          通過上面 TS 內(nèi)部實現(xiàn)的高級類型可以發(fā)現(xiàn),extendsinfer 是特別重要的。extends 可以實現(xiàn)類似三元表達式的判斷,判斷傳入的泛型是什么類型的,然后返回定義好的類型。infer 可以在判斷是什么類型后,可以提取其中的類型并在子句中使用。和我們正常寫代碼一樣,說明我們可以多個 extendsinfer 進行嵌套使用,這樣就可以把一個傳入的泛型進行一次次分解。

          協(xié)變與逆變

          在了解協(xié)變與逆變之前我們需要知道一個概念——子類型。我們前面提到過 string 可以賦值給 unknown 那么就可以理解為 stringunknown 的子類型。正常情況下這個關(guān)系即子類型可以賦值給父類型是不會改變的我們稱之為協(xié)變,但是在某種情況下兩者會出現(xiàn)顛倒我們稱這種關(guān)系為逆變。如:

                
                interface?Animal?{
          ??name:?string;
          }

          interface?Cat?extends?Animal?{
          ??catBark:?string;
          }

          interface?OrangeCat?extends?Cat?{
          ??color:?'orange'
          }

          //?ts?中不一定要使用繼承關(guān)系,只要是?A?的類型在?B?中全部都有,且?B?比?A?還要多一些類型
          //?類似集合 A 屬于 B 一樣,這樣就可以將 B 叫做 A 的子類型。

          //?以上從屬關(guān)系
          //?OrangeCat?是?Cat?的子類型
          //?Cat?是?Animal?的子類型
          //?同理?OrangeCat?也是?Animal?的子類型

          const?cat:?Cat?=?{
          ??name:?'貓貓',
          ??catBark:?'喵~~'
          }
          const?animal:?Animal?=?cat;?//?no?error

          假設(shè)我有類型 type FnCat = (value: Cat) => Cat; 請問下面四個誰是它的子類型,即以下那個類型可以賦值給它。

                
                type?FnAnimal?=?(value:?Animal)?=>?Animal;
          type?FnOrangeCat?=?(value:?OrangeCat)?=>?OrangeCat;
          type?FnAnimalOrangeCat?=?(value:?Animal)?=>?OrangeCat;
          type?FnOrangeCatAnima?=?(value:?OrangeCat)?=>?Animal;

          type?RES1?=?FnAnimal?extends?FnCat???true?:?false;?//?false
          type?RES2?=?FnOrangeCat?extends?FnCat???true?:?false;?//?false
          type?RES3?=?FnAnimalOrangeCat?extends?FnCat???true?:?false;?//?true
          type?RES4?=?FnOrangeCatAnima?extends?FnCat???true?:?false;?//?false

          為什么 RES3 是可以的吶?

          返回值:假設(shè)使用了 FnCat 返回值的 cat.catBark 屬性,如果返回值是 Animal 則不會有這個屬性,會導(dǎo)致調(diào)用出錯。估計返回值只能是 OrangeCat

          參數(shù):假設(shè)傳入的函數(shù)中使用了 orangeCat.color 但是,對外的類型參數(shù)還是 Cat 沒有 color 屬性,就會導(dǎo)致該函數(shù)運行時內(nèi)部報錯。

          故可以得出結(jié)論:返回值是協(xié)變,入?yún)⑹悄孀儭?/strong>

          注意如果 tsconfig.json 中的 strictFunctionTypesfalse 則上述的 RES2 也是 true ,這就表明當(dāng)前函數(shù)是支持雙向協(xié)變的。當(dāng)然 TS 默認是關(guān)閉此選項的,主要是為了方便 JS 代碼快速遷移到 TS 中,詳情可以見 why-are-function-parameters-bivariant[9] ,當(dāng)然如果是一個新項目,建議打開 strictFunctionTypes 選項。

          允許雙向協(xié)變是有風(fēng)險的,可能會在運行時報錯。比如在 ESLint 中有 method-signature-style[10] 規(guī)則,簡單的來說該規(guī)則默認是使用 property 來聲明方法,比如:

                
                interface?T1?{
          ??wrapFn:?(value:?T)?=>?void;
          }

          interface?T2?{
          ??wrapFn(value:?T):?void;?//?eslint?error,?聲明的方式是?method?形式
          }

          假設(shè)我們忽略 eslint 警告,強制 T2 的方法進行聲明就會潛在的雙向協(xié)變的風(fēng)險,如下列代碼:

                
                declare?let?animalT1:?T1;
          declare?let?catT1:?T1

          animalT1?=?catT1;?//?error,?Animal?不能分配給?Cat
          catT1?=?animalT1;?//?no?error

          declare?let?animalT2:?T2;
          declare?let?catT2:?T2

          animalT2?=?catT2;?//?no?error
          catT2?=?animalT2;?//?no?error
          真實案例

          聯(lián)合類型轉(zhuǎn)交叉類型

          題目描述

                
                type?Value?=?{?a:?string?}?|?{?b:?number?}
          type?Res?=?UnionToIntersection?//?type?Res=?{??a:?string?}?&?{?b:?number?};

          思路

          1.              [Distributive Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types "Distributive Conditional Types") :當(dāng)條件類型作用于泛型類型時,它們在給定聯(lián)合類型時變得可分配。
                      
                
                //?extend?中的分配
          type?ToArray?=?Type?extends?any???Type[]?:?never;
          type?StrArrOrNumArr?=?ToArray<string?|?number>;?//?string[]?|?number[]
          1.              [逆變的特性:在逆變位置時推斷出交叉類型]( "逆變的特性:在逆變位置時推斷出交叉類型")
                      
                
                //?逆變推斷出交叉
          type?Bar?=?T?extends?{?a:?(x:?infer?U)?=>?void,?b:?(x:?infer?U)?=>?void?}???U?:?never;
          type?T20?=?Bar<{?a:?(x:?string)?=>?void,?b:?(x:?string)?=>?void?}>;??//?string
          type?T21?=?Bar<{?a:?(x:?string)?=>?void,?b:?(x:?number)?=>?void?}>;??//?string?&?number

          結(jié)合二者

                
                type?ToUnionFunction?=?T?extends?unknown???(x:?T)?=>?void?:?never;
          type?UnionToIntersection?=?ToUnionFunction?extends?(x:?infer?R)?=>?unknown
          ??????????R
          ????????:?never

          測試

                
                type?Res?=?UnionToIntersection?//?type?Res=?{??a:?string?}?&?{?b:?number?};

          增強版 Omit

          題目描述

          假如我們要輕微的修改某個組件亦或者是拓展組件的某個屬性,比如下面代碼中的 size ,想給他增加一個 default 的配置項,但是其他的 props 都不會改變。首先肯定不想進行復(fù)制粘貼,其次也為了后續(xù)內(nèi)部的組件 props 變更后,外層的邏邏輯不改。

          第一印象就是繼承然后修改 size 的值,但是很遺憾因為新的類型與已有的類型不兼容,所以不能覆蓋成功。所以謀生了第二種思路,想把 size 給排除出去,然后重寫。但是也不行,因為拓展的組件為了方便添加了 [key: string]: unknown,導(dǎo)致 omit 有問題。

                
                interface?Props?{
          ??title:?string;
          ??size:?'small'?|?'large';
          ??[key:?string]:?unknown;
          }

          interface?NewProps1?extends?Props?{
          ??size:?'small'?|?'large'?|?'default';?//?error:?不能將?default?分配給?'small'?和?'large'
          }

          interface?NewProps2?extends?Omit?{
          ??size:?'small'?|?'large'?|?'default';
          }

          const?a:?NewProps2?=?{
          ??title:?123,?//?no?error,?但是我們知道?title?類型丟失了
          ??size:?'default',
          }

          思路

          因為是添加了 [key: string]: unknown 后才導(dǎo)致 omit 失效的,根據(jù)上面 omit 的實現(xiàn)我們可以知道實際上是 keyof 方法不能處理 [key: string] 這樣的索引簽名,根據(jù)常識對象索引簽名的類型只支持 string、number、symbol 和字面量。所以只需要保留字面量的 key 就行了。

                
                /**
          ?*?我們已知
          ?*??1.?K?只會為?string?|?number?|?symbol?|?字面量
          ?*??2.?string?extends?K?時,只有?K?為?string?時才是?true,?同理這里可以檢查出?number?和?symbol?然后?as?為?never.
          ?*/

          type?KnownKeys?=?keyof?{
          ??[K?in?keyof?T?as?(
          ????string?extends?K
          ????????never
          ??????:?number?extends?K
          ??????????never
          ????????:?symbol?extends?K
          ????????????never
          ??????????:?K)
          ??]:?never;
          };

          /**
          ?*?因為?Pick?的第二個參數(shù)需要?K?extends?keyof?T
          ?*?所以這里需要判斷?KnownKeys?extends?keyof?T,
          ?*/

          export?type?ObtainKeyOmitextends?KnownKeys>?=?KnownKeys?extends?keyof?T???Pick,?K>>?:?never;

          測試

                
                interface?NewProps3?extends?ObtainKeyOmit?{
          ??size:?'small'?|?'large'?|?'default';
          ??[key:?string]:?unknown;?//?因為?ObtainKeyOmit?去掉了,所以需要加回來
          }

          const?a:?NewProps3?=?{
          ??title:?123,?//?error,?number?不能賦值給?string
          ??size:?'default',
          ??name:?'licy',
          }

          下劃線轉(zhuǎn)駝峰

          題目描述

          根據(jù)目前大多數(shù)的規(guī)范來說,服務(wù)端開發(fā)的同學(xué)大多數(shù)是使用下劃線命名,而前端的同學(xué)是用小駝峰命名。所以大部分同學(xué)會在 axios 請求數(shù)據(jù)回來的 hooks 中使用 camelcase-keys 將數(shù)據(jù)中的下劃線轉(zhuǎn)換成小駝峰。但是大多數(shù)接口的類似是使用 thrift 進行轉(zhuǎn)換的,所以生成的類型文件也是下劃線構(gòu)造的。所以可以完成一個方法將下劃線轉(zhuǎn)換成小駝峰。

                
                //?thrift?的構(gòu)造,key?為下劃線,其中值有可能是數(shù)組或者對象的嵌套
          interface?User?{
          ??user_id:?string;
          ??name:?string;
          ??user_status?:?number;
          ??avatar_url:?{
          ????default_url:?string;
          ????small_url?:?string;
          ????large_url?:?string;
          ??};
          ??colleague:?{
          ????user_name:?string;
          ????user_id:?string;
          ????user_status?:?number;
          ??}[];
          };

          思路

          1. 首先需要完成下劃線字符串轉(zhuǎn)駝峰的方法 Under2camel

            1. 使用 T extends `${infer F}_${infer L}` 的方式可以獲取 _ 前后的值
            2. 使用 Capitalize 可以將字符串的首字母大寫
            3. 注意兼容 _xx 開頭的數(shù)據(jù)
          2.              `Under2camelDeep` 的方法其實只是根據(jù)值的類型添加的遞歸處理
                      
            1. key 進行轉(zhuǎn)換,如果是字符串就使用 Under2camel 方法,如果是 numbersymbol 就跳過。
            2. 使用 T[K] extends (infer O)[] 判斷是數(shù)組,并且還可以提取其中的值
                
                //?只做字符串的轉(zhuǎn)換
          type?Under2camelextends?string>?=?T?extends?`${infer?F}_${infer?L}`
          ????F?extends?''
          ??????Under2camel
          ????:`${F}${Under2camel>}`
          ??:?T;
          type?AAA?=?Under2camel<'_user_id_ddd_aaa_'>;?//?userIdDddAaa

          //?遞歸遍歷
          type?Under2camelDeep?=?T?extends?(infer?U)[]
          ????Under2camelDeep[]
          ??:?{
          ????[K?in?keyof?T?as?(K?extends?string???Under2camel?:?never)]:?T[K]?extends?Record<string,?unknown>
          ????????Under2camelDeep
          ????????:?T[K]?extends?(infer?O)[]
          ????????????Under2camelDeep[]
          ??????????:?T[K]
          ??}

          測試

                
                const?data:?Under2camelDeep?=?{
          ??userId:?'id001',
          ??name:?'licy',
          ??userStatus:?0,
          ??avatarUrl:?{
          ????smallUrl:?'https://xxx',
          ????defaultUrl:?'https://xxx'
          ??},
          ??colleague:?[
          ????{
          ??????userName:?'zhangsan',
          ??????userId:?'id002',
          ??????userStatus:?0,
          ????}
          ??]
          };

          課后作業(yè)

          編寫一個 Under2camelDeep 方法的相反方法,Camel2underDeep 可以將小駝峰命名轉(zhuǎn)換成 _ 連接。

          加強版 Pick

          題目描述

          很多時候我們會使用 lodashpick 方法進行深度的選取,如 _.pick(o, ['a', 'b.c', 'b.e[0].a'])。所以也希望可以提供一個 DeepPick 的方法可以進行深度的選取。

                
                interface?User?{
          ??name:?string;
          ??address:?{
          ????country?:?string;
          ????city:?string;
          ??};
          ??spend:?{
          ????price:?number;
          ????description?:?string;
          ??}[];
          }

          type?NewUser?=?DeepPick'name'?|?'address.city'?|?'spend[0].price'>;

          思路

          1. 前文提到過,聯(lián)合類型具有分配的性質(zhì),可以想象成一個數(shù)組,每一次只會有一個值代入表達式計算,最后的結(jié)果也是一個聯(lián)合類型。
          2. 使用 infer 的方式去提取字符串

          3. 由于 [0].. 有共同的部分,需要先判斷 [0].

          4. 遞歸即可

          5. 得到的數(shù)據(jù)是 {name: string} | { address: { city: BJ } } 的形式,并不是一個交叉的類型。

          6.              使用前文提到的 `UnionToIntersection` 方法將聯(lián)合類型轉(zhuǎn)換成交叉類型。
                      
                
                type?_DeepPick?=?U?extends?`${infer?F}[0].${infer?Rest}`
          ????F?extends?keyof?T
          ??????T[F]?extends?(infer?O)[]
          ????????{?[P?in?F]:?DeepPick[]?}
          ??????:?never
          ????:?never
          ??:?U?extends?`${infer?F}.${infer?Rest}`
          ??????F?extends?keyof?T
          ????????{?[P?in?F]:?DeepPick?}
          ??????:?never
          ????:?U?extends?keyof?T
          ????????{?[P?in?U]:?T[U]?}
          ??????:?never;

          type?DeepPick?=?UnionToIntersection<_DeepPick>;

          測試

                
                type?NewUser?=?DeepPick'name'?|?'address.city'?|?'spend[0].price'>;
          //?{
          //???name:?string;
          //???address:?{
          //?????city:?string;
          //???};
          //???spend:?{
          //?????price:?number;
          //???}[]
          //?}

          const?a:?NewUser?=?{
          ??name:?'licy',
          ??address:?{
          ????city:?'BJ',
          ??},
          ??spend:?[
          ????{
          ??????price:?100,
          ????},
          ??],
          };

          課后作業(yè)

          1.              如果加入 `address.country` 后,由于該項是一個可選項,所以可以不寫,但是目前解析成了 `string | undefinde` 所以不寫會進行報錯,如何修復(fù)。
                      
                
                type?NewUser?=?DeepPick'name'?|?'address.city'?|?'spend[0].price'>;
          //?{
          //???name:?string;
          //???address:?{
          //?????country:?string?|?undefined;
          //?????city:?string;
          //???};
          //???spend:?{
          //?????price:?number;
          //???}[]
          //?}

          const?a:?NewUser?=?{
          ??name:?'licy',
          ??address:?{?//?報錯,缺少?country?類型
          ????city:?'BJ',
          ??},
          ??spend:?[
          ????{
          ??????price:?100,
          ????},
          ??],
          };
          1. 修改 DeepPick 中的 U 可以讓輸入的時候和 Pick 方法一樣,有提示。

          Object.assign 類型提示

          題目描述

          Object.assign 是一個常用 API 方法,但是目前來說這個方法自動推斷的類型是有問題的,因為他是采用 type1 & type2 的方式,所以如果 2 個類型沒有共有屬性,就會得到 never。

                
                type?O1?=?{
          ??id:?string;
          ??value:?string;
          ??age?:?number;
          ??extra?:?{
          ????a:?number;
          ????type:?'a';
          ??};
          };

          type?O2?=?{
          ??name:?string;
          ??value:?number;
          ??city?:?string;
          ??extra?:?{
          ????type:?'b';
          ??};
          };

          type?O3?=?{
          ??city:?string[];
          };

          const?o1:?O1?=?{
          ??id:?'id1',
          ??value:?'abc',
          ??age:?24,
          ??extra:?{
          ????a:?1,
          ????type:?'a',
          ??},
          };

          const?o2:?O2?=?{
          ??name:?'licy',
          ??value:?0,
          ??city:?'BJ',
          ??extra:?{
          ????type:?'b',
          ??},
          };

          const?o3:?O3?=?{
          ??city:?['bj'],
          };

          const?res?=?Object.assign({},?o1,?o2,?o3);
          //?類型丟失了
          res.value?//?never
          res.extra?//?never
          res.city?//?string?&?string[]

          思路

          因為 & 是取交集,而這里 assign 的能力是覆蓋所以不能直接 &。應(yīng)該判斷如果后面的類型中有,則前面的類型就不需要有了。所以在遍歷 T1 時,如果該 key 存在于 T2 中,則不應(yīng)該有此項。

                
                type?Mergeextends?Record<string,?unknown>,?T2?extends?Record<string,?unknown>>?=?{
          ??[K?in?keyof?T1?as?K?extends?keyof?T2???never?:?K]:?T1[K];
          }?&?{
          ??[K?in?keyof?T2]:?T2[K];
          };

          type?Assignextends?Record<string,?unknown>,?U>?=?U?extends?[infer?F,?...infer?Rest]
          ????F?extends?Record<string,?unknown>
          ??????Assign,?Rest>
          ????:?Assign
          ??:?T;

          測試

                
                const?res:?Assign?=?Object.assign({},?o1,?o2,?o3);
          //?類型推斷正確
          res.value?//?number
          res.extra?//?{?type:?'b'?}
          res.city?//?string[]

          課后作業(yè)

          1.              目前這個方法不是完美的,存在一定的缺陷,假設(shè) `O3` 的類型中有未必填的 `key` 值,然后就會導(dǎo)致類型推斷出現(xiàn)問題。如:
                      
                
                type?O3?=?{
          ??id?:?number;
          ??city:?string[];
          };

          const?res:?Assign?=?Object.assign({},?o1,?o2,?o3);
          res.id?//?id?:?number?|?undefined?,有問題?因為?O1?是肯定有?id,所以推斷出了問題

          如何完善 Merge 方法,使得上面的 res.id 自動推斷為 string | number。

          1. 如果是深度的 assgin 如何實現(xiàn)?比如我期望上文中的 res.extra 推斷為 { a: number; type:'b' }
          周邊工具

          推薦工具

          1. 在線代碼練習(xí):TS 代碼演練場[11]
          2. 本地練習(xí):VSCode 編輯器

            1. 默認情況下是使用 VSCode 的 TS 版本進行,所以可能造成編寫時的提示和編譯版本不一致。可以點擊圖中的 {} ,然后選擇 TS 版本。也可以通過 command+shift+p 調(diào)出命令界面,輸入 typescript 進行選擇。
          8dda7cbb3ae59164e1d7b5a30b96c1bf.webp
          1. 很多時候我們在代碼中編寫工具函數(shù),然后等著頁面刷新是很麻煩的,我們可以在一個測試 TS 中間中進行編寫,編寫完成后直接把代碼復(fù)制過去。直接運行 TS 的工具 ts-node[12],可以監(jiān)聽變化熱更新 ts-node-dev[13]。當(dāng)然也可以用目前最新的工具 bun[14]。

          fd2e7fb44d854e2edf5c96c81e60dd81.webpd008fb98eb9713da4b6ac481789387a6.webp

          1. 很多常用的 TS 類型工具庫,可以看成和 lodash 類似,如果有不知道如何寫的可以參考。TS 類型工具庫[15]

          2. Lint 檢查工具,以前有專門的 TS Lint 但是由于 TS LintES Lint 過程高度的相似,所以目前 TSLint 被并入了 ES lintTS Lint 也被官方標記為了放棄維護。所以可以安裝:\@typescript-eslint/eslint-plugin[16] 和 \@typescript-eslint/parser[17],使用 ESLint 進行代碼風(fēng)格的檢測。

          3. 類型覆蓋檢查工具,可以使用 type-coverage[18] 進行項目中類型覆蓋度的檢測。特別適合從一個 JS 項目遷移到 TS 項目的過程中,得到階段性的數(shù)據(jù),當(dāng)然也可以作為一個 MR 的準入標準。比如下面就是目前低代碼三個核心包的類型覆蓋率,還是有一點提升空間的。

          d049746f9093c5225d98ea24d1adfc2a.webp

          推薦學(xué)習(xí)資料

          TypeScript 官方使用手冊 [19]

          深入理解 TypeScript [20]

          強烈推薦type-challenges[21]

          類型體操天花板是怎樣煉成的 - Web Infra 團隊定期技術(shù)分享 [22]

          用 TypeScript 類型運算實現(xiàn)一個中國象棋程序 [23]

          TypeScript 類型體操天花板,用類型運算寫一個 Lisp 解釋器 [24]

          \[? 全技巧解析史詩典藏 ?\]用 TypeScript 類型寫一個 Lisp 解釋器 Pro (尾遞歸優(yōu)化版) [25]

          參考資料

          [1]

          安德斯·海爾斯伯格: https://baike.baidu.com/item/%E5%AE%89%E5%BE%B7%E6%96%AF%C2%B7%E6%B5%B7%E5%B0%94%E6%96%AF%E4%BC%AF%E6%A0%BC

          [2]

          Roadmap: https://github.com/microsoft/TypeScript/wiki/Roadmap

          [3]

          v4.7: https://github.com/microsoft/TypeScript/issues/48027

          [4]

          Understanding TypeScript's Popularity: https://orta.io/notes/js/why-typescript

          [5]

          Flow: https://flow.org/

          [6]

          JSDoc: https://jsdoc.app/

          [7]

          尤玉溪的回答: https://www.zhihu.com/question/46397274/answer/101193678

          [8]

          utility-types: https://www.typescriptlang.org/docs/handbook/utility-types.html

          [9]

          why-are-function-parameters-bivariant: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant

          [10]

          method-signature-style: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/method-signature-style.md

          [11]

          TS 代碼演練場: https://www.typescriptlang.org/zh/play

          [12]

          ts-node: https://www.npmjs.com/package/ts-node

          [13]

          ts-node-dev: https://www.npmjs.com/package/ts-node-dev

          [14]

          bun: https://bun.sh/

          [15]

          TS 類型工具庫: https://github.com/millsp/ts-toolbelt

          [16]

          @typescript-eslint/eslint-plugin: https://www.npmjs.com/package/@typescript-eslint/eslint-plugin

          [17]

          @typescript-eslint/parser: https://www.npmjs.com/package/@typescript-eslint/parser

          [18]

          type-coverage: https://www.npmjs.com/package/type-coverage

          [19]

          TypeScript 官方使用手冊: https://www.typescriptlang.org/docs/handbook/

          [20]

          深入理解 TypeScript: https://jkchao.github.io/typescript-book-chinese/

          [21]

          type-challenges: https://github.com/type-challenges/type-challenges

          [22]

          類型體操天花板是怎樣煉成的 - Web Infra 團隊定期技術(shù)分享: https://bytedance.feishu.cn/minutes/obcnbl4cae2792wz7vw78lom

          [23]

          用 TypeScript 類型運算實現(xiàn)一個中國象棋程序: https://zhuanlan.zhihu.com/p/426966480

          [24]

          TypeScript 類型體操天花板,用類型運算寫一個 Lisp 解釋器: https://zhuanlan.zhihu.com/p/427309936

          [25]

          [?全技巧解析史詩典藏?]用 TypeScript 類型寫一個 Lisp 解釋器 Pro (尾遞歸優(yōu)化版): https://bytedance.feishu.cn/docs/doccnf74joJlJKkfOVNjsCyy6Gb


          往期回顧
          #

          如何使用 TypeScript 開發(fā) React 函數(shù)式組件?

          #

          11 個需要避免的 React 錯誤用法

          #

          6 個 Vue3 開發(fā)必備的 VSCode 插件

          #

          3 款非常實用的 Node.js 版本管理工具

          #

          6 個你必須明白 Vue3 的 ref 和 reactive 問題

          #

          6 個意想不到的 JavaScript 問題

          #

          試著換個角度理解低代碼平臺設(shè)計的本質(zhì)

          回復(fù)“加群”,一起學(xué)習(xí)進步

          瀏覽 58
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                    青青草网 | 中文字幕精品一区久久久久 | 黄色视频免费收看黄色视频免费收看 | 操逼免费观看视频 | av最新版天堂资源在线 |