<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>

          一文理解TypeScript中各種高級語法

          共 20634字,需瀏覽 42分鐘

           ·

          2022-07-31 08:28

          推薦關(guān)注↓

          引言

          TypeScript 的重要性我不在強調(diào)了,我相信仍然會有大多數(shù)前端開發(fā)者碰到復雜類型一概使用 any 處理。

          我寫這篇文章的目的就是為了讓你告別 AnyScript ,文章告別晦澀的概念結(jié)合實例來為你講述一系列 TS 高級用法:分發(fā)、循環(huán)、協(xié)變、逆變、unknown ... 等等之類。讓我們告別枯燥的概念,結(jié)合真實用例來掌握 TypeScript 從此徹底告別 AnyScript !

          文章并不會從基礎的 TS 語法開始講解,如果你還不了解什么是 TypeScript 強烈建議閱讀?TS 官方文檔[1]。

          泛型

          泛型基礎

          熟悉 Java 或者 C# 的朋友對于 泛型的概念也許非常了解,關(guān)于泛型的概念這里我并不打算在文章中進行贅述了。

          關(guān)于如何解釋泛型,我看到的最好的一句話概括把明確類型的工作推遲到創(chuàng)建對象或調(diào)用方法的時候才去明確的特殊的類型,簡單點來講我們可以將泛型理解成為把類型當作參數(shù)一樣去傳遞。

          比如這樣一個簡單的例子:

          function?identity<T>(arg:?T):?T?{?
          ??return?arg;
          }

          //?調(diào)用identity時傳入name,函數(shù)會自動推導出泛型T為string,自然arg類型為T,返回值類型也為T
          const?userName?=?identity('name');
          //?同理,當然你也可以顯示聲明泛型
          const?id?=?identity<number>(1);?

          它在 TS 中的確非常重要,同時也有許多非常優(yōu)秀的文章來講述它的基礎用法。它既重要又基礎,是掌握 TS 高級用法的重中之重。

          如果你目前還不是非常了解泛型,那么強烈建議你去閱讀?Generics Type[2]。

          接口泛型位置

          之所以將接口中的泛型單獨拉出來和大家講述,是因為在日常工作中經(jīng)常會碰到一些同事對于泛型接口位置的不理解。

          空口無憑,我們來看看這樣一個簡單的例子:

          //?定義一個泛型接口?IPerson表示一個類,它返回的實例對象取決于使用接口時傳入的泛型T
          interface?IPerson<T>?{??
          ??//?因為我們還沒有講到unknown?所以暫時這里使用any?代替??
          ??new(...args:?unknown[]):?T;
          }

          function?getInstance<T>(Clazz:?IPerson<T>)?{?
          ??return?new?Clazz();
          }

          //?use?it
          class?Person?{}

          //?TS推斷出函數(shù)返回值是person實例類型
          const?person?=?getInstance(Person);

          上邊的 Demo 是一個非常再不同不過的例子了,我們定義接口 IPerson 時,這個接口定義了一個泛型參數(shù) T 表示返回的實例類型。

          當使用時,我們需要在使用接口時聲明該 T 類型,比如IPerson<T>

          接下來我們在看對比另外一個例子:

          //?聲明一個接口IPerson代表函數(shù)
          interface?IPerson?{?
          ??//?此時注意泛型是在函數(shù)中參數(shù)?而非在IPerson接口中?
          ??<T>(a:?T):?T;
          }

          //?函數(shù)接受泛型
          const?getPersonValue:?IPerson?=?<T>(a:?T):?T?=>?{??
          ??return?a;
          };

          //?相當于getPersonValue<number>(2)
          getPersonValue(2)

          這里上下兩個例子特別像強調(diào)的是關(guān)于泛型接口中泛型的位置是代表完全不同的含義:

          • 當泛型出現(xiàn)在接口中時,比如interface IPerson<T>?代表的是使用接口時需要傳入泛型的類型,比如IPerson<T>。
          • 當泛型出現(xiàn)在接口內(nèi)部時,比如第二個例子中的?IPerson接口代表一個函數(shù),接口本身并不具備任何泛型定義。而接口代表的函數(shù)則會接受一個泛型定義。換句話說接口本身不需要泛型,而在實現(xiàn)使用接口代表的函數(shù)類型時需要聲明該函數(shù)接受一個泛型參數(shù)。

          趁熱打鐵,我們來看這樣一個例子:當我們希望實現(xiàn)一個數(shù)組的 forEach 方法時,嘗試使用泛型來實現(xiàn):

          //?定義callback遍歷方法?兩種方式?應該采用哪一種?
          type?Callback?=?<T>(item:?T)?=>?void
          //?第二種聲明方式
          type?Callback<T>?=?(item:?T)?=>?void;

          const?forEach?=?<T>(arr:?T[],?callback:?Callback)?=>?{?
          ??for?(let?i?=?0;?i?<?arr.length?-?1;?i++)?{??
          ????callback(arr[i])?
          ??}
          };

          forEach(['1',?2,?3,?'4'],?(item)?=>?{});

          關(guān)于 forEach 方法我相信大伙兒都已經(jīng)非常了解了,這里我們嘗試使用 TS 來實現(xiàn)這個方法。此時我們將 callback 類型單獨抽離出來。

          上邊的寫法有兩種聲明方式,小伙伴們覺得應該關(guān)于 forEach 中的 callback 類型定義應該采用第幾種呢?第一種 or 第二種?

          大家可以結(jié)合上邊的兩個例子自己先稍微思考下。

          答案是第二種方式type Callback<T> = (item: T) => void;。

          這里有一個非常關(guān)鍵的點需要注意,所謂 TS 是一種靜態(tài)類型檢測,并不會執(zhí)行你的代碼。

          我們先來分析第二種方式的類型定義,我稍微將調(diào)用時的代碼補充完整(這樣方便大伙兒理解):

          //?item的類型取決于調(diào)用函數(shù)時傳入的類型參數(shù)
          type?Callback?=?<T>(item:?T)?=>?void;

          const?forEach?=?<T>(arr:?T[],?callback:?Callback)?=>?{?
          ??for?(let?i?=?0;?i?<?arr.length?-?1;?i++)?{???
          ????//?這里調(diào)用callback時,ts并不會執(zhí)行我們的代碼。???
          ????//?換句話說:它并不清楚arr[i]是什么類型???
          ????callback(arr[i]);?
          ??}
          };

          //?所以這里我們并不清楚?callback?定義中的T是什么類型,自然它的類型還是T
          forEach(['1',?2,?3,?'4'],?<T>(item:?T)?=>?{});

          如果采用第二種聲明方式,在 forEach 內(nèi)部的 callback 函數(shù)調(diào)用時,才會清楚函數(shù)傳入的參數(shù)類型。顯然forEach 調(diào)用時無法正確推斷出 item 的類型定義。

          接下來,我們來看看第二種方式:

          //?item?的類型取決于使用類型時傳入的泛型參數(shù)
          type?Callback<T>?=?(item:?T)?=>?void;

          //?在聲明階段就已經(jīng)確定了?callback?接口中的泛型參數(shù)為外部傳入的
          const?forEach?=?<T>(arr:?T[],?callback:?Callback<T>)?=>?{?
          ??for?(let?i?=?0;?i?<?arr.length?-?1;?i++)?{??
          ????callback(arr[i]);
          ??}
          };

          //?自然,我們在調(diào)用forEach時顯式聲明泛型參數(shù)為?string?|?number?類型
          //?所以根據(jù)forEach的函數(shù)類型定義時,
          //?自然?callback?的?item?也會在定義時被推導為?T?也就是所謂的?string?|?number?類型
          forEach<string?|?number>(['1',?2,?3,?'4'],?(item)?=>?{});

          所以,這一點在日常開發(fā)中希望小伙伴們一定要特別留意:在泛型接口中泛型的聲明位置不同所產(chǎn)生的效果是完全不同的。

          泛型約束

          所謂泛型約束,通俗點來講就是約束泛型需要滿足的格式。提到它,有一個非常經(jīng)典的案例:

          //?定義方法獲取傳入?yún)?shù)的length屬性
          function?getLength<T>(arg:?T)?{??
          ??//?throw?error:?arr上不存在length屬性?
          ??return?arg.length;
          }

          這里,我們定義了一個 getLength 方法,希望函數(shù)獲取傳入?yún)?shù)的 length 屬性。

          因為傳入的參數(shù)是不固定的,有可能是 string 、 array 、 arguments 對象甚至一些我們自己定義的?{ name:"19Qingfeng", length: 100 },所以我們?yōu)楹瘮?shù)增加泛型來為函數(shù)增加更加靈活的類型定義。

          可是隨之而來的問題來了,那么此時我們在函數(shù)內(nèi)部訪問了 arg.length 屬性。但是此時,arg 所代表的泛型可以是任意類型。

          比如我們可以傳入一個 boolean ,那么此時函數(shù)中的泛型 T 代表 boolean 類型,訪問 boolean.length ? 這顯然是一個 bug 。

          那么如果解決這個問題呢,當然就提到了所謂的泛型約束 extends 關(guān)鍵字。

          我們先來看看如何使用它:

          interface?IHasLength?{?
          ??length:?number;
          }

          //?利用?extends?關(guān)鍵字在聲明泛型時約束泛型需要滿足的條件
          function?getLength<T?extends?IHasLength>(arg:?T)?{?
          ??//?throw?error:?arr上不存在length屬性?
          ??return?arg.length;
          }

          getLength([1,?2,?3]);?//?correct
          getLength('123');?//?correct
          getLength({?name:?'19Qingfeng',?length:?100?});?//?correct
          //?error?當傳入true時,TS會進行自動類型推導?相當于?getLength<boolean>(true)
          //?顯然?boolean?類型上并不存在擁有?length?屬性的約束,所以TS會提示語法錯誤getLength(true);?

          類型關(guān)鍵字

          其實原本一些簡單的類型關(guān)鍵字我并不打算在文章中去闡述的,但是后續(xù)許多高級類型以及高級概念正是基于這些來實現(xiàn)的,所以文章為了照顧一些不是特別熟悉 TS 的小伙伴還是對于這部分特意進行了講述。

          keyof 關(guān)鍵字

          所謂 keyof 關(guān)鍵字代表它接受一個對象類型作為參數(shù),并返回該對象所有 key 值組成的聯(lián)合類型。

          比如:

          interface?IProps?{?
          ??name:?string;
          ??age:?number;?
          ??sex:?string;
          }

          //?Keys?類型為?'name'?|?'age'?|?'sex'?組成的聯(lián)合類型
          type?Keys?=?keyof?IProps

          看上去非常簡單對吧,需要額外注意的一點是當?keyof any?時候我們會得到什么類型呢?

          小伙伴們可以稍微思考下?keyof any?會得到什么樣的類型。

          //?Keys?類型為?string?|?number?|?symbol?組成的聯(lián)合類型
          type?Keys?=?keyof?any

          其實這是非常容易理解,any 可以代表任何類型。那么任何類型的 key 都可能為 string 、 number 或者 symbol 。所以自然 keyof any 為 string | number | symbol 的聯(lián)合類型。

          在之后的高級類型中會利用到keyof any的特性,所以這里提前拿出來讓大家預熱下。

          在了解了 keyof 關(guān)鍵字之后,讓我們結(jié)合泛型來實現(xiàn)一個簡單的例子來練練手。

          比如此時,我們希望實現(xiàn)一個函數(shù)。該函數(shù)希望接受兩個參數(shù),第一個參數(shù)為一個對象object,第二個參數(shù)為該對象的 key 。函數(shù)內(nèi)部通過傳入的 object 以及對應的 key 返回?object[key]?。

          function?getValueFromKey(obj:?object,?key:?string)?{?
          ??//?throw?error?
          ??//?key的值為string代表它僅僅只被規(guī)定為字符串??
          ??//?TS無法確定obj中是否存在對應的key
          ??return?obj[key];
          }

          顯然,我們直接為參數(shù)聲明類型這是會報錯的。同學們可以結(jié)合剛剛學過的 keyof 關(guān)鍵字配合泛型來思考一下如何消除 TS 的錯誤提示。

          //?函數(shù)接受兩個泛型參數(shù)
          //?T?代表object的類型,同時T需要滿足約束是一個對象
          //?K?代表第二個參數(shù)K的類型,同時K需要滿足約束keyof?T?(keyof?T?代表object中所有key組成的聯(lián)合類型)
          //?自然,我們在函數(shù)內(nèi)部訪問obj[key]就不會提示錯誤了
          function?getValueFromKey<T?extends?object,?K?extends?keyof?T>(obj:?T,?key:?K)?{?
          ??return?obj[key];
          }

          它的實現(xiàn)非常簡單,這里沒有寫出來的同學可以好好看看文章中上述的內(nèi)容。

          is 關(guān)鍵字

          原本是不打算講述這個基礎概念的,奈何之前在一次面試中因為 is 關(guān)鍵字翻了車哈哈。

          面試官問我熟悉 Ts 嗎,答案一定是肯定的。結(jié)果問了我一個 is 關(guān)鍵字代表的含義,當時的我簡直是百思不得其解.. “難道你問的不是 as 嗎”,is 究竟是個什么東西好像從來沒有聽說過。

          于是面試結(jié)束后趕快搜了搜,結(jié)果竟然就是業(yè)務中經(jīng)常用到的類型謂詞。。。

          所謂 is 關(guān)鍵字其實更多用在函數(shù)的返回值上,用來表示對于函數(shù)返回值的類型保護。

          它的用法非常簡單:

          //?函數(shù)的返回值類型中?通過類型謂詞?is?來保護返回值的類型
          function?isNumber(arg:?any):?arg?is?number?{?
          ??return?typeof?arg?===?'number'
          }

          function?getTypeByVal(val:any)?{?
          ??if?(isNumber(val))?{??
          ????//?此時由于isNumber函數(shù)返回值根據(jù)類型謂詞的保護??
          ????//?所以可以斷定如果?isNumber?返回true?那么傳入的參數(shù)?val?一定是?number?類型???
          ????val.toFixed()?
          ??}
          }

          所以,通常我們使用 is 關(guān)鍵字(類型謂詞)在函數(shù)的返回值中,從而對于函數(shù)傳入的參數(shù)進行類型保護。

          infer 關(guān)鍵字

          infer 關(guān)鍵字我不打算放在這里和大家描述了,我會在下面的內(nèi)容和大家逐步切入對應的關(guān)鍵字。

          TS 高級概念

          分發(fā)

          在講述分發(fā)的概念,我會先和你聊聊 TS 中的 Conditional Types (條件類型)。

          因為大多數(shù)高級類型都是基于條件類型,同時分發(fā)的概念也和 Conditional Types 息息相關(guān),所以我們先來看看所謂的 Conditional Types 究竟是什么。

          type?isString<T>?=?T?extends?string???true?:?false;

          //?a?的類型為?true
          let?a:?isString<'a'>

          //?b?的類型為?false
          let?b:?isString<1>;

          上邊我們通過 type 關(guān)鍵字定義了一個所謂的 isString 類型,它接受一個泛型參數(shù) T 。

          isString 類型內(nèi)部通過 extends 關(guān)鍵字結(jié)合 ? 和 : 實現(xiàn)了所謂的 Conditional Types (條件類型)判斷。

          type isString<T> = T extends string ? true : false;

          稍微翻譯翻譯上邊這段代碼,當泛型 T 滿足 string 類型的約束時,它會返回 true ,否則則會返回 false 類型。

          其實所謂的條件類型就是這么簡單,看起來和三元表達式非常相似,甚至你完全可以將它理解成為三元表達式。只不過它接受的是類型以及判斷的是類型而已。

          需要額外注意的是:

          • 這里的?T extends string?更像是一種判斷泛型 T 是否滿足 string 的判斷,和之前所講的泛型約束完全不是同一個意思。

          上述我們講的泛型約束是在定義泛型時進行對于傳入泛型的約束,而這里的?T extends string ? true : false?并不是在傳入泛型時進行的約束。

          在使用 isString 時,你可以為它傳入任意類型作為泛型參數(shù)的實現(xiàn)。但是 isString 類型內(nèi)部會對于傳入的泛型類型進行判斷,如果 T 滿足 string 的約束條件,那么返回類型 true,反過來則是 false 。

          • 其次,需要注意的是條件類型?a extends b ? c : d?僅僅支持在 type 關(guān)鍵字中使用。

          在了解了泛型約束之后,我們在回到所謂分發(fā)的概念上來。

          一起來看看這樣一個例子:

          type?GetSomeType<T?extends?string?|?number>?=?T?extends?string???'a'?:?'b';

          let?someTypeOne:?GetSomeType<string>?//?someTypeone?類型為?'a'

          let?someTypeTwo:?GetSomeType<number>?//?someTypeone?類型為?'b'

          let?someTypeThree:?GetSomeType<string?|?number>;?//?what???

          這里我們定義了一個 GetSomeType 的類型,它接受一個泛型參數(shù) T 。這個泛型參數(shù) T 在傳入時需要滿足為 string 和 number 的聯(lián)合類型的約束。(換句話說,要么為 string 要么為 number 要么為 string | number)。

          • 首先,someTypeOne 變量的類型為?GetSomeType<string>。

          因為我們?yōu)?someTypeOne 定義時傳入了 string 的類型參數(shù),所以按照條件類型來判斷,string extends string?明顯是滿足的,所以返回類型 'a'。

          • 同理,someTypeTwo 變量的類型也會被推斷為 'b',這個過程我就不在累贅了。

          那么重點來了,someTypeThree 定義時的類型 GetSomeType<'string' | 1> 我們傳入的泛型參數(shù)為聯(lián)合類型時 'string' | 1 時,它會的到什么類型呢?

          首先不難想象,我們按照正常邏輯來思考。'string' | 1 一定是不滿足?T extends string,因為一個?'string' | 1?的聯(lián)合類型一定是無法和 string 類型做兼容的。

          那么按照我們之前的邏輯來梳理,理所應當 someTypeThree 的類型應該是 'b' 對吧。

          可是結(jié)果真如那么簡單的話,那么我還舉出來這個例子做什么呢?

          4b0256ad763647a2d50dffc0441a9eb3.webp

          很驚訝吧,someTypeThree 的類型竟然被推導成為了 'a' | 'b' 組成的聯(lián)合類型,那么為什么會這樣呢。

          其實這就是所謂分發(fā)在搗鬼。

          我們拋開晦澀的概念來解讀分發(fā),結(jié)合上邊的 Demo 來說所謂的分發(fā)簡單來說就是分別使用 string 和 number 這兩個類型進入 GetSomeType 中進行判斷,最終返回兩次類型結(jié)果組成的聯(lián)合類型。

          當然,你可以在使用 GetSomeType 你可以傳入n個類型組成的聯(lián)合類型作為泛型參數(shù),同理它會進行進入 GetSomeType 類型中進行 n 次分發(fā)判斷。

          //?你可以這樣理解分發(fā)
          //?偽代碼:GetSomeType<string | number>?= GetSomeType<string>?| GetSomeType<number>
          let?someTypeThree:?GetSomeType<string?|?number>

          自然我們就得到的 someTypeThree 類型為 "a" | "b" 。

          相信看到這里的同學都已經(jīng)能理解分發(fā)代表的是什么含義。

          那么,什么情況下會產(chǎn)生分發(fā)呢?滿足分發(fā)需要一定的條件,我們來一起看看:

          • 首先,毫無疑問分發(fā)一定是需要產(chǎn)生在 extends 產(chǎn)生的類型條件判斷中,并且是前置類型。

          比如T extends string | number ? 'a' : 'b';?那么此時,產(chǎn)生分發(fā)效果的也只有 extends 關(guān)鍵字前的 T 類型,string | number 僅僅代表一種條件判斷。

          • 其次,分發(fā)一定是要滿足聯(lián)合類型,只有聯(lián)合類型才會產(chǎn)生分發(fā)(其他類型無法產(chǎn)生分發(fā)的效果,比如 & 交集中等等)。
          • 最后,分發(fā)一定要滿足所謂的裸類型中才會產(chǎn)生效果。

          這里的裸類型稍微和大家解釋下,比如這樣:

          //?此時的T并不是一個單獨的”裸類型“T?而是?[T]
          type?GetSomeType<T?extends?string?|?number?|?[string]>?=?[T]?extends?string[]??
          ????'a'?
          ??:?'b';
          ??
          //?即使我們修改了對應的類型判斷,仍然不會產(chǎn)生所謂的分發(fā)效果。因為[T]并不是一個裸類型
          //?只會產(chǎn)生一次判斷??[string]?|?number?extends?string[]????'a'?:?'b'
          //?someTypeThree?仍然只有?'b'?類型?,如果進行了分發(fā)的話那么應該是?'a'?|?'b'
          let?someTypeThree:?GetSomeType<[string]?|?number>;

          同樣,在了解了分發(fā)的概念和清楚了如何會產(chǎn)生分發(fā)的效果后。趁熱打鐵我們來看看利用分發(fā)我們可以實現(xiàn)什么樣的效果:

          在 TypeScript 內(nèi)部擁有一個高級內(nèi)置類型 Exclude 意為排除,它的用法如下:

          type?TypeA?=?string?|?number?|?boolean?|?symbol;

          //?ExcludeSymbolType?類型為?string?|?number?|?boolean,排除了symbol類型
          type?ExcludeSymbolType?=?Exclude<TypeA,?symbol>;

          用法非常簡單,Exclude 內(nèi)置類型會接受兩個類型泛型參數(shù)。它會構(gòu)造一個新的類型,這個類型會排除所有 TypeA 類型中滿足 symbol 的類型。

          那么,如果讓你來實現(xiàn)一個 Exclude 內(nèi)置類型,你會如何實現(xiàn)呢?同學們可以結(jié)合分發(fā)自行思考下。

          如果沒有想出來的小伙伴,強烈建議在重新好好溫習一下分發(fā)這個章節(jié)。

          type?TypeA?=?string?|?number?|?boolean?|?symbol;

          type?MyExclude<T,?K>?=?T?extends?K???never?:?T;

          //?ExcludeSymbolType?類型為?string?|?number?|?boolean,排除了symbol類型

          type?ExcludeSymbolType?=?MyExclude<TypeA,?symbol?|?boolean>;

          其實它的實現(xiàn)非常簡單,上述我們通過分發(fā)來實現(xiàn)了對應的 MyExclude 類型。

          MyExclude 類型接受兩個泛型參數(shù),因為?T extends K ? never : T?中 T 滿足裸類型并且在 extends 關(guān)鍵字前。

          同時,我們傳入的 TypeA 為聯(lián)合類型,那么滿足分發(fā)的所有條件。則會產(chǎn)生分發(fā)效果,也就是說會將聯(lián)合類型 TypeA 中所有的單個類型依次進入?T extends K ? never : T;?去判斷。

          當滿足條件時,也就是?T extends symbol | boolean?時,此時會得到 never 。(這里的 never 代表的也就是一個無法達到的類型,不會產(chǎn)生任何效果),自然就會被忽略。

          而如果不滿足?T extends symbol | boolean?則會被記錄,最終返回不滿足?T extends symbol | boolean?的所有類型組成的聯(lián)合類型,也就是所謂的?string | number?。

          當然和 Exclude 相反效果的內(nèi)置類型?Extract[3]、NonNullable[3]也是基于分發(fā)實現(xiàn)的,有興趣的小伙伴可以自行查閱實現(xiàn)。

          循環(huán)

          TypeScript 中同樣存在對于類型的循環(huán)語法(Mapping Type),通過我們可以通過 in 關(guān)鍵字配合聯(lián)合類型來對于類型進行迭代。比如這樣:

          interface?IProps?{
          ??name:?string;
          ??age:?number;?
          ??highSchool:?string;??
          ??university:?string;
          }

          //?IPropsKey類型為
          //?type?IPropsKey?=?{
          //??name:?boolean;
          //??age:?boolean;
          //??highSchool:?boolean;
          //??university:?boolean;
          //??}
          type?IPropsKey?=?{?[K?in?keyof?IProps]:?boolean?};

          其實相對來說循環(huán)關(guān)鍵字 in 比較簡單,上述代碼我們聲明了一個所謂的 IPropsKey 的類型:

          首先可以看到這個類型是一個對象,對象中的 key 為?[]?包裹的可計算值,value 為 boolean。

          keyof IProps?我們在之前提到過它會返回 IProps 所有 key 組成的聯(lián)合類型,也就是?'name' | 'age' | 'highSchool' | 'university'?。

          而?[K in keyof IProps]?正是我們在類型內(nèi)部聲明了一個變量 K 。

          你可以理解為 in 關(guān)鍵字的作用類似于 for 循環(huán),它會循環(huán) keyof IProps 這個聯(lián)合類型中的每一項類型,同時在每一次循環(huán)中將對應的類型賦值給 K 。

          最終,通過一次一次循環(huán)我們到了最終的新類型?interface IProps { name: string; age: number; highSchool: string; university: string; }?。

          那么在 TS 中我們可以利用循環(huán)的特性來做什么呢?不知道大家有沒有用到 Partial 之類的內(nèi)置類型。

          所謂的 Partial 功能非常簡單:它會構(gòu)造一個新的類型,這個類型會將之前類型中的所有屬性都變?yōu)榭蛇x。

          比如這樣:

          4c5c5c96da6945d1bdceeffe063a25d3.webp

          可以看到我們通過 Partial 傳入 IInfo 類型,它返回一個新類型 OptinalInfo,OptinalInfo 會將 IInfo 中所有的屬性都變?yōu)榭蛇x類型。

          同樣,大家可以自己動手來實現(xiàn)一下它。其實結(jié)合我們剛剛說到的循環(huán)來實現(xiàn)會非常簡單。

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

          type?MyPartial<T>?=?{?[K?in?keyof?T]?:?T[K]?};

          type?OptionalInfo?=?MyPartial<IInfo>;

          看起來非常簡單對吧,我們通過 in 循環(huán)傳入的 IInfo,同時構(gòu)造了一個新的類型它的 key 和 IInfo 是一模一樣的。

          只不過僅僅是所有屬性 key 是可選的,而非必填。

          當然需要注意的是我們剛才提到的所有關(guān)鍵字,比如 extends 進行條件判斷或者 in 進行類型循環(huán)時,僅僅支持在 type 類型聲明中使用,并不可以在 interface 中使用,這也是 type 和 interface 聲明的一個不同。

          當然,還有許多內(nèi)置類型同樣利用了循環(huán),比如 Required、Readonly 等等。

          同學們可以思考下如果讓你實現(xiàn)一個 Required 應該如何實現(xiàn),它會利用到一些Mapping Modifiers[4](映射修飾符),有興趣的朋友可以實現(xiàn)一下練練手。

          當然在循環(huán)的最后,我們來思考另一個問題。其實你會發(fā)現(xiàn)無論是 TS 內(nèi)置的 Partial 還是我們剛剛自己實現(xiàn)的 Partial ,它僅僅對象中一層的轉(zhuǎn)化并不能遞歸處理。

          比如說:

          interface?IInfo?{?
          ??name:?string;?
          ??age:?number;??
          ??school:?{??
          ????middleSchool:?string;???
          ????highSchool:?string;??
          ????university:?string;?
          ??}
          }

          type?OptionalInfo?=?Partial<IInfo>;
          9ed5e54ceeaadc484bc33d85235b2afb.webp

          可以看到利用 Partial 關(guān)鍵字僅僅對于對象類型中的最外層進行了可選標記。

          但是對于內(nèi)層嵌套類型比如 school 仍是一個對象類型,那么此時是無法深度進入 school 類型中進行標記的。

          那么假如此時我有需求希望實現(xiàn)深度可選,應該如何做呢?大家可以往上邊提到過的條件判斷和循環(huán)結(jié)合來考慮下。

          interface?IInfo?{?
          ??name:?string;??
          ??age:?number;?
          ??school:?{???
          ????middleSchool:?string;???
          ????highSchool:?string;??
          ????university:?string;?
          ??};
          }

          //?其實實現(xiàn)很簡單,首先我們在構(gòu)造新的類型value時
          //?利用?extends?條件判斷新的類型value是否為?object?
          //?如果是?->?那么我仍然利用?deepPartial<T[K]>?進行包裹遞歸可選處理
          //?如果不是?->?普通類型直接返回即可
          type?deepPartial<T>?=?{?
          ??[K?in?keyof?T]?:?T[K]?extends?object???deepPartial<T[K]>?:?T[K];
          };

          type?OptionalInfo?=?deepPartial<IInfo>;

          let?value:?OptionalInfo?=?{?
          ??name:?'1',?
          ??school:?{??
          ????middleSchool:'xian'?
          ??},
          };

          不賣關(guān)子了,如果對于文章之前的知識點你都掌握了,那么我相信實現(xiàn)這個功能對你來說簡直是小菜一碟。

          其實看到這里,TS 內(nèi)置的一些類型比如 Pick 、 Omit 大家都可以嘗試自己去實現(xiàn)下了。我們之前說到了知識點已經(jīng)可以完全涵蓋這些內(nèi)置類型的實現(xiàn)。

          逆變

          許多不是很熟悉 TS 的朋友對于逆變和協(xié)變的概念會感到莫名的恐懼,沒關(guān)系。它們僅僅代表闡述表現(xiàn)的概念而已,放心我們并不會從概念入手而是通過實例來逐步為你揭開它的面紗。

          首先,我們先來思考這樣一個場景:

          let?a!:?{?a:?string;?b:?number?};
          let?b!:?{?a:?string?};

          b?=?a

          我們都清楚 TS 屬于靜態(tài)類型檢測,所謂類型的賦值是要保證安全性的。

          通俗來說也就是多的可以賦值給少的,上述代碼因為 a 的類型定義中完全包括 b 的類型定義,所以 a 類型完全是可以賦值給 b 類型,這被稱為類型兼容性。

          之后,我們再來思考這樣一段代碼:

          let?fn1!:?(a:?string,?b:?number)?=>?void;
          let?fn2!:?(a:?string,?b:?number,?c:?boolean)?=>?void;

          fn1?=?fn2;?//?TS?Error:?不能將fn2的類型賦值給fn1

          我們將 fn2 賦值給 fn1 ,剛剛才提到類型兼容性的原因 TS 允許不同類型進行互相賦值(只需要父/子集關(guān)系),那么明明 fn2 的參數(shù)包括了所有的 fn1 為什么會報錯?

          上述的問題,其實和剛剛沒有什么本質(zhì)區(qū)別。我們來換一個角度來理解這個問題:

          針對于 fn1 聲明時,函數(shù)類型需要接受兩個參數(shù),換句話說調(diào)用 fn1 時我需要支持兩個參數(shù)的傳入分別是?a:stringb:number。

          同理 fn2 函數(shù)定義時,定義了三個參數(shù)那么調(diào)用 fn2 時自然也需要傳入三個參數(shù)。

          那么此時,我們將 fn2 賦值給 fn1 ,我們可以思考下。如果賦值成功了,當我調(diào)用 fn1 時,其實相當于調(diào)用 fn2 沒錯吧。

          但是,由于 fn1 的函數(shù)類型定義僅僅支持兩個參數(shù)?a:stringb:number?即可。但是由于我們執(zhí)行了?fn1 = fn2。

          調(diào)用 fn1 時,實際相當于調(diào)用了 fn2 函數(shù)。但是類型定義上來說 fn1 滿足兩個參數(shù)傳入即可,而 fn2 是實打?qū)嵉男枰獋魅?3 個參數(shù)。

          那么此時,如果執(zhí)行了 fn1 = fn2 當調(diào)用 fn1 時明顯參數(shù)個數(shù)會不匹配(由于類型定義不一致)會缺少一個第三個參數(shù),顯然這是不安全的,自然也不是被 TS 允許的。

          那么反過來呢?

          let?fn1!:?(a:?string,?b:?number)?=>?void;
          let?fn2!:?(a:?string,?b:?number,?c:?boolean)?=>?void;

          fn2?=?fn1;?//?正確,被允許

          按照剛才的思路來分析,我們將 fn1 賦值給 fn2 。fn2 的類型定義需要支持三個參數(shù)的傳入,但實際 fn2 內(nèi)部指針已經(jīng)被修改稱為 fn1 的指針。

          fn1 在執(zhí)行時僅僅需要兩個參數(shù)?a: string, b: number,顯然 fn2 的類型定義中是滿足這個條件的(當然它還多傳遞了第三個參數(shù)?c:boolean,在 JS 中對于函數(shù)而言調(diào)用時的參數(shù)個數(shù)大于定義時的參數(shù)個數(shù)是被允許的)。

          自然,這是安全的也是被 TS 允許賦值。

          就比如上述函數(shù)的參數(shù)類型賦值就被稱為逆變,參數(shù)少(父)的可以賦給參數(shù)多(子)的那一個??雌饋砗皖愋图嫒菪裕ǘ嗟目梢再x給少的)相反,但是通過調(diào)用的角度來考慮的話恰恰滿足多的可以賦給少的兼容性原則。

          上述這種函數(shù)之間互相賦值,他們的參數(shù)類型兼容性是典型的逆變[5]。

          我們再來看一個稍微復雜點的例子來加深所謂逆變的理解:

          class?Parent?{}

          //?Son繼承了Parent?并且比parent多了一個實例屬性?name
          class?Son?extends?Parent?{?
          ??public?name:?string?=?'19Qingfeng';
          }

          //?GrandSon繼承了Son?在Son的基礎上額外多了一個age屬性
          class?Grandson?extends?Son?{?
          ??public?age:?number?=?3;
          }

          //?分別創(chuàng)建父子實例
          const?son?=?new?Son();

          function?someThing(cb:?(param:?Son)?=>?any)?{?
          ??//?do?some?someThing?
          ??//?注意:這里調(diào)用函數(shù)的時候傳入的實參是Son
          ??cb(Son);
          }

          someThing((param:?Grandson)?=>?param);?//?error
          someThing((param:?Parent)?=>?param);?//?correct

          這里我們定義了三個類,他們之間的關(guān)系分別是 Parent 是基類,Son 繼承 Parent ,Grandson 繼承 Son 。

          同時我們定義了一個函數(shù),它接受一個 cb 回調(diào)參數(shù)作為參數(shù),我們定義了這個回調(diào)函數(shù)的類型為接受一個 param 為 Son 實例類型的參數(shù),此時我們不關(guān)心它的返回值給一個 any 即可。

          注意這里,我們先用剛才的結(jié)論來推導。剛才我們提到過函數(shù)的參數(shù)的方式被稱為逆變,所以當我們調(diào)用 someThing 時傳遞的 callback 需要賦給定義 something 函數(shù)中的 cb 。

          換句話說類型?(param: Grandson) => param?需要賦給?cb: (param: Son) => any,這顯然是不被允許的。

          因為逆變的效果函數(shù)的參數(shù)只允許“從少的賦值給多的”,顯然 Grandson 相較于 Son 來說多了一個 name 屬性少,所以這是不被允許的。

          相反,第二個someThing((param: Parent) => param);相當于函數(shù)參數(shù)重將 Parent 賦給 Son 將少的賦給多的滿足逆變,所以是正確的。

          之后我們在嘗試分析為什么第二個someThing((param: Parent) => param);是正確的。

          首先我們需要注意到我們在定義 someThing 函數(shù)時,聲明了這個函數(shù)接受一個 cb 的函數(shù)。這個函數(shù)接受一個類型為 Son 的參數(shù)。

          someThing 內(nèi)部cb 函數(shù)聲明時需要滿足 Son 的參數(shù),它會在 cb 函數(shù)調(diào)用時傳入一個 Son 參數(shù)的實參。

          所以當我們傳入?someThing((param: Parent) => param)?時,相當于在 something 函數(shù)內(nèi)部調(diào)用?(param: Parent) => param?時會根據(jù) someThing 中callback的定義傳入一個 Son 。

          那么此時,我們函數(shù)真實調(diào)用時期望得到是 Parent,但是實際得到了 Son 。Son 是 Parent 的子類涵蓋所有 Parent 的公共屬性方法,自然也是滿足條件的。

          反而言之,當我們使用someThing((param: Grandson) => param);?,由于 something 定義 cb 的類型傳入 Son,但是真實調(diào)用 someThing 時,我們確需要一個 Grandson 類型參數(shù)的函數(shù),這顯然是不符合的。

          關(guān)于逆變我用了比較多的篇幅去描述它,我希望通過文章大家都可以對于逆變結(jié)合實例來理解并應用它。因為它的確稍微有些繞。

          協(xié)變

          解決了逆變之后,其實協(xié)變對于大伙兒來說都是小意思。我們先來看看這個 Demo:

          let?fn1!:?(a:?string,?b:?number)?=>?string;
          let?fn2!:?(a:?string,?b:?number)?=>?string?|?number?|?boolean;

          fn2?=?fn1;?//?correct?
          fn1?=?fn2?//?error:?不可以將?string|number|boolean?賦給?string?類型

          這里,函數(shù)類型賦值兼容時函數(shù)的返回值就是典型的協(xié)變場景,我們可以看到 fn1 函數(shù)返回值類型規(guī)定為 string,fn2 返回值類型規(guī)定為 string | number | boolean 。

          顯然 string | number | boolean 是無法分配給 string 類型的,但是 string 類型是滿足 string | number | boolean 其中之一,所以自然可以賦值給 string | number | boolean 組成的聯(lián)合類型。

          其實這就是協(xié)變....當然你也可以嘗試從函數(shù)運行角度來解讀協(xié)變的概念,比如當 fn1 運行結(jié)束要求返回 string , fn2 運行結(jié)束后要求返回 string | number | boolean 。

          將 fn1 賦給 fn2 ,fn1 要求返回值是 string ,而真實調(diào)用的 fn1=fn2 相當于調(diào)用了 fn2 自然 string | number | boolean 無法滿足string類型的要求,所以 TS 會認為這是錯誤的。

          待推斷類型

          infer 代表待推斷類型,它的必須和 extends 條件約束類型一起使用。

          之前,我們在 類型關(guān)鍵字中遺留了 infer 關(guān)鍵字并沒有展開講述,這里我們了解了所謂的 extends 代表的類型約束之后我們來一起看看所謂 infer 帶來的待推斷類型效果。

          在條件類型約束中為我們提供了 infer 關(guān)鍵字來提供實現(xiàn)更多的類型可能,它表示我們可以在條件類型中推斷一些暫時無法確定的類型,比如這樣:

          type?Flatten<Type>?=?Type?extends?Array<infer?Item>???Item?:?Type;

          上述我們定義了一個 Flatten 類型,它接受一個傳入的泛型 Type ,我們在類型定義內(nèi)部對于傳入的泛型 Type 進行了條件約束:

          • 如果 Type 滿足?Array<infer Item>,那么此時返回 Item 類型。
          • 如果 Type 不滿足?Array<infer Item>類型,那么此時返回 Type 類型。

          關(guān)于如何理解?Array<infer Item>,一句話描述就是我們利用 infer 聲明了一個數(shù)組類型,數(shù)組中值的類型我們并不清楚所以使用 infer 來進行推斷數(shù)組中的值。

          比如:

          465980627c90a7711b58d5713c910de8.webp

          我們?yōu)轭愋虵latten傳入一個 string 類型,顯然傳入的 string 并不滿足數(shù)組的約束。自然直接返回傳入的 string 類型。

          此時我們試試傳入一個數(shù)組類型呢:

          ddf9003bc13a276c33cfa19875cae971.webp

          可以看到返回的 subType 類型為 string | number 。我們來稍微分析這一過程:

          聲明Flatten<[string, number]>時,F(xiàn)latten 接受到一個?[string,number]?的泛型參數(shù)。

          顯然?[string,number]是滿足數(shù)組的條件的,Type extends Array<infer Item>。

          所謂的?Array<infer Item>代表的進行條件判斷時要求前者(Type)必須是一個數(shù)組,但是數(shù)組中的類型我并不清楚(或者說可以是任意)。

          自然我們使用 infer 關(guān)鍵字表示待推斷的類型, infer 后緊跟著類型變量 Item 表示的就是待推斷的數(shù)組元素類型。

          我們類型定義時并不能立即確定某些類型,而是在使用類型時來根據(jù)條件來推斷對應的類型。之后,因為數(shù)組中的元素可能為 string 也可能為 number,自然在使用類型時 infer Item 會將待推斷的 Item 推斷為 string | number 聯(lián)合類型。

          需要注意的是 infer 關(guān)鍵字類型,必須結(jié)合 Conditional Types 條件判斷來使用。

          那么,在條件類型中結(jié)合 infer 會幫助我們帶來什么樣的作用呢?我們一起來看看 infer 的實際用法。

          在 TS 中存在一個內(nèi)置類型 Parameters ,它接受傳入一個函數(shù)類型作為泛型參數(shù)并且會返回這個函數(shù)所有的參數(shù)類型組成的元祖。

          //?定義函數(shù)類型
          interface?IFn?{?
          ??(age:?number,?name:?string):?void;
          }

          //?type?FnParameters?=?[age:?number,?name:?string]
          type?FnParameters?=?Parameters<IFn>;

          let?a:?FnParameters?=?[25,?'19Qingfeng'];

          它的內(nèi)部實現(xiàn)恰恰是利用 infer 來實現(xiàn)的,同學們可以自己嘗試來實現(xiàn)這個內(nèi)置類型。

          type?MyParameters<T?extends?(...args:?any)?=>?any>?=?T?extends?(??
          ??...args:?infer?R
          )?=>?any?
          ????R?
          ??:?never;

          其實它的實現(xiàn)非常簡單,定義的 MyParameters 類型中接受一個泛型 T 當傳入 T 時需要滿足它為函數(shù)類型的約束。

          其次我們在 MyParameters 內(nèi)部對于 傳入的泛型參數(shù)進行了條件判斷,如果滿足條件也就是?T extends ( ...args: infer R ) => any,需要注意的是條件判斷中函數(shù)的參數(shù)并不是在類型定義時就確認的,函數(shù)的參數(shù)需要根據(jù)傳入的泛型來確認后賦給變量 R 所以使用了 infer R 來表示待推斷的函數(shù)參數(shù)類型。

          那么此時我會返回滿足條件的函數(shù)推斷參數(shù)組成的數(shù)組也就是 ...args 的類型 R ,否則則返回 never 。

          當然 TS 內(nèi)部還存在比如 ReturnType 、ThisParameterType 等類型都是基于條件判斷中的 infer 來推斷出結(jié)果的,有興趣的朋友可以自行查閱。

          日常工作中,我們經(jīng)常會碰到將元祖轉(zhuǎn)化成為聯(lián)合類型的需求,比如?['a',1,true]?我們希望快速得到元組中元素的類型應該如何實現(xiàn)呢?

          0c81da0506dbb3aefbc9661b6c485553.webp

          unknown & any

          在 TypeScript 中同樣存在一個高級類型 unknown ,它可以代表任意類型的值,這一點和 any 是非常類型的。

          但是我們清楚將類型聲明為 any 之后會跳過任何類型檢查,比如這樣:

          let?myName:?any;

          myName?=?1

          //?這明顯是一個bug
          myName()

          而 unknown 和 any 代表的含義完全是不一樣的,雖然 unknown 可以和 any 一樣代表任意類型的值,但是這并不代表它可以繞過 TS 的類型檢查。

          let?myName:?unknown;

          myName?=?1

          //?ts?error:?unknown?無法被調(diào)用,這被認為是不安全的
          myName()

          //?使用typeof保護myName類型為function
          if?(typeof?myName?===?'function')?{?
          ??//?此時myName的類型從unknown變?yōu)?span style="color:rgb(198,120,221);">function??
          ??//?可以正常調(diào)用?
          ??myName()
          }

          通俗來說 unknown 就代表一些并不會繞過類型檢查但又暫時無法確定值的類型,我們在一些無法確定函數(shù)參數(shù)(返回值)類型中 unknown 使用的場景非常多。比如:

          //?在不確定函數(shù)參數(shù)的類型時
          //?將函數(shù)的參數(shù)聲明為unknown類型而非any
          //?TS同樣會對于unknown進行類型檢測,而any就不會
          function?resultValueBySome(val:unknown)?{?
          ??if?(typeof?val?===?'string')?{??
          ????//?此時?val?是string類型???
          ????//?do?someThing?
          ??}?else?if?(typeof?val?===?'number')?{?
          ????//?此時?val?是number類型???
          ????//?do?someThing??
          ??}?
          ??//?...
          }

          當然,在描述了 unknown 類型的含義之后,關(guān)于 unknown 類型有一個特別重要的點我想和大家強調(diào):

          e50baf836fddae13c4616fd967b8b54c.webp

          unknown類型可以接收任意類型的值,但并不支持將unknown賦值給其他類型。

          any類型同樣支持接收任意類型的值,同時賦值給其他任意類型(除了never)。

          any 和 unknown 都代表任意類型,但是 unknown 只能接收任意類型的值,而 any 除了可以接收任意類型的值,也可以賦值給任意類型(除了 never)。

          比如下面這樣:

          let?a!:?any;
          let?b!:?unknown;

          //?任何類型值都可以賦給any、unknown
          a?=?1;
          b?=?1;

          //?callback函數(shù)接受一個類型為number的參數(shù)
          function?callback(val:?number):?void?{}

          //?調(diào)用callback傳入aaa(any)類型?correct
          callback(a);

          //?調(diào)用callback傳入b(unknown)類型給?val(number)類型?error
          //?ts?Error:?類型“unknown”的參數(shù)不能賦給類型“number”的參數(shù)callback(b);

          當然,對于以后并不確定類型的變量希望大家盡量使用更多的 unknown 來代替 any 讓你的代碼更加強壯。

          寫在結(jié)尾

          至此,文章對于 TypeScript 的內(nèi)容就在這里和大家告一段落了。

          感謝每一位看到這里的小伙伴,其實關(guān)于如何精進 TypeScript 功底在我個人看來可以總結(jié)為以下兩點:

          第一,碰到問題一定是要結(jié)合文檔多查閱文檔(當然 TypeScript 一定是要去嘗試閱讀英文文檔,及時你的英語不是那么好),它的中文文檔實在是過于簡陋了。

          第二,關(guān)于 TS 中的確存在對于普通開發(fā)者太多的陌生概念。掌握它最直接的辦法就是去用 TS 在任何你能用到的地方,哪怕只是一個特別小的項目,正所謂所謂熟能生巧嘛。

          參考資料

          [1]

          TS 官方文檔:?https://www.typescriptlang.org/docs/handbook/2/basic-types.html

          [2]

          Generics Type:?https://www.typescriptlang.org/docs/handbook/2/generics.html

          [3]

          Extract:??https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union

          [4]

          Mapping Modifiers:?https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers

          [5]

          逆變:?https://zh.wikipedia.org/wiki/協(xié)變與逆變

          作者:19組清風

          https://juejin.cn/post/7089809919251054628


          關(guān)注我,一起攜手進階


          瀏覽 37
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产成人精品免费视频 | 日韩中文欧美 | 欧美一交一乱一交一色一色情 | wwwwxxxx黄色在线观看 | 男人色色网 |