偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

你不知道的 TypeScript 泛型

開(kāi)發(fā) 前端
軟件工程中,我們不僅要?jiǎng)?chuàng)建一致的定義良好的 API,同時(shí)也要考慮可重用性。組件不僅能夠支持當(dāng)前的數(shù)據(jù)類(lèi)型,同時(shí)也能支持未來(lái)的數(shù)據(jù)類(lèi)型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能。

[[429322]]

一、泛型是什么

軟件工程中,我們不僅要?jiǎng)?chuàng)建一致的定義良好的 API,同時(shí)也要考慮可重用性。組件不僅能夠支持當(dāng)前的數(shù)據(jù)類(lèi)型,同時(shí)也能支持未來(lái)的數(shù)據(jù)類(lèi)型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能。

泛型表示泛指某一種類(lèi)型,開(kāi)發(fā)者可以指定一個(gè)表示類(lèi)型的變量,用它來(lái)作為實(shí)際類(lèi)型的占位符,用尖括號(hào)來(lái)包裹類(lèi)型變量 。泛型的主要作用是創(chuàng)建可重用的組件,從而讓一個(gè)組件可以支持多種數(shù)據(jù)類(lèi)型,它可以作用在接口、類(lèi)、函數(shù)或類(lèi)型別名上。

下面我們來(lái)舉個(gè)例子,幫助大家更好地理解上述的內(nèi)容。在這個(gè)例子中,我們將一步步揭示泛型的作用。

1.1 identity 函數(shù)示例

首先我們來(lái)定義一個(gè)通用的 identity 函數(shù),該函數(shù)接收一個(gè)參數(shù)并直接返回它:

  1. function identity (value) { 
  2.   return value; 
  3.  
  4. console.log(identity(1)) // 1 

現(xiàn)在,我們將 identity 函數(shù)做適當(dāng)?shù)恼{(diào)整,以支持 TypeScript 的 number 類(lèi)型的參數(shù):

  1. function identity (value: number) : number { 
  2.  
  3. return value; 
  4.  
  5.  
  6. console.log(identity(1)) // 1 

這里 identity 的問(wèn)題是我們將 number 類(lèi)型分配給參數(shù)和返回類(lèi)型,使該函數(shù)僅可用于該原始類(lèi)型。但該函數(shù)并不是通用的,很明顯這并不是我們所希望的。雖然我們可以把 number 換成 any,但這樣的話(huà),我們失去了定義應(yīng)該返回哪種類(lèi)型的能力,并且在這個(gè)過(guò)程中使編譯器失去了類(lèi)型保護(hù)的作用。

我們的目標(biāo)是讓 identity 函數(shù)可以適用于任意的類(lèi)型,為了實(shí)現(xiàn)這個(gè)目標(biāo),我們可以使用泛型,具體實(shí)現(xiàn)方式如下:

  1. function identity <T>(value: T) : T { 
  2.   return value; 
  3.  
  4. console.log(identity<number>(1)) // 1 

對(duì)于剛接觸 TypeScript 泛型的讀者來(lái)說(shuō),首次看到 語(yǔ)法會(huì)感到陌生。但這沒(méi)什么可擔(dān)心的,就像傳遞參數(shù)一樣,我們傳遞了我們想要用于特定函數(shù)調(diào)用的類(lèi)型。

參考上面的圖片,當(dāng)我們調(diào)用 identity(1) ,number 類(lèi)型就像參數(shù) 1 一樣,它將在出現(xiàn) T 的任何位置填充該類(lèi)型。圖中 內(nèi)部的 T 被稱(chēng)為類(lèi)型變量,它是我們希望傳遞給 identity 函數(shù)的類(lèi)型占位符,同時(shí)它被分配給 value 參數(shù)用來(lái)代替它的類(lèi)型:此時(shí) T 充當(dāng)?shù)氖穷?lèi)型,而不是特定的 number 類(lèi)型。

其中 T 代表 Type,在定義泛型時(shí)通常用作第一個(gè)類(lèi)型變量名稱(chēng)。但實(shí)際上 T 可以用任何有效名稱(chēng)代替。除了 T 之外,以下是常見(jiàn)泛型變量代表的意思:

  • K(Key):表示對(duì)象中的鍵類(lèi)型;
  • V(Value):表示對(duì)象中的值類(lèi)型;
  • E(Element):表示元素類(lèi)型。

其實(shí)并不是只能定義一個(gè)類(lèi)型變量,我們可以引入希望定義的任何數(shù)量的類(lèi)型變量。比如我們引入一個(gè)新的類(lèi)型變量 U,用于擴(kuò)展我們定義的 identity 函數(shù):

  1. function identity <T, U>(value: T, message: U) : T { 
  2.   console.log(message); 
  3.   return value; 
  4.  
  5. console.log(identity<Number, string>(68, "Semlinker")); 

除了為類(lèi)型變量顯式設(shè)定值之外,另一種方式是讓編譯器自動(dòng)選擇這些類(lèi)型,從而使代碼更簡(jiǎn)潔。我們可以完全省略尖括號(hào),比如:

  1. function identity <T, U>(value: T, message: U) : T { 
  2.   console.log(message); 
  3.   return value; 
  4.  
  5. console.log(identity(68, "Semlinker")); 

對(duì)于上述代碼,編譯器足夠聰明,能夠知道我們的參數(shù)類(lèi)型,并將它們賦值給 T 和 U,而不需要開(kāi)發(fā)人員顯式指定它們。下面我們來(lái)看張動(dòng)圖,直觀(guān)地感受一下類(lèi)型傳遞的過(guò)程:

如你所見(jiàn),該函數(shù)接收你傳遞給它的任何類(lèi)型,使得我們可以為不同類(lèi)型創(chuàng)建可重用的組件?,F(xiàn)在我們?cè)賮?lái)看一下 identity 函數(shù):

  1. function identity <T, U>(value: T, message: U) : T { 
  2.   console.log(message); 
  3.   return value; 

相比之前定義的 identity 函數(shù),新的 identity 函數(shù)增加了一個(gè)類(lèi)型變量 U,但該函數(shù)的返回類(lèi)型我們?nèi)匀皇褂? T。如果我們想要返回兩種類(lèi)型的對(duì)象該怎么辦呢?針對(duì)這個(gè)問(wèn)題,我們有多種方案,其中一種就是使用 TypeScript 中的元組:

  1. function identity <T, U>(value: T, message: U) : [T, U] { 
  2.   return [value, message]; 

二、泛型接口

要解決函數(shù)中返回多種類(lèi)型對(duì)象的問(wèn)題,我們可以創(chuàng)建一個(gè)用于的 identity 函數(shù)通用 Identities 接口:

  1. interface Identities<V, M> { 
  2.   value: V, 
  3.   message: M 

在上述的 Identities 接口中,我們引入了類(lèi)型變量 V 和 M,來(lái)進(jìn)一步說(shuō)明有效的字母都可以用于表示類(lèi)型變量,之后我們就可以將 Identities 接口作為 identity 函數(shù)的返回類(lèi)型:

  1. function identity<T, U> (value: T, message: U): Identities<T, U> { 
  2.   console.log(value + ": " + typeof (value)); 
  3.   console.log(message + ": " + typeof (message)); 
  4.   let identities: Identities<T, U> = { 
  5.     value, 
  6.     message 
  7.   }; 
  8.   return identities; 
  9.  
  10. console.log(identity(68, "Semlinker")); 

以上代碼成功運(yùn)行后,在控制臺(tái)會(huì)輸出以下結(jié)果:

  1. 68: number 
  2. Semlinker: string 
  3. {value: 68, message: "Semlinker"

泛型除了可以應(yīng)用在函數(shù)和接口之外,它也可以應(yīng)用在類(lèi)中,下面我們就來(lái)看一下在類(lèi)中如何使用泛型。

三、泛型類(lèi)

在類(lèi)中使用泛型也很簡(jiǎn)單,我們只需要在類(lèi)名后面,使用 的語(yǔ)法定義任意多個(gè)類(lèi)型變量,具體示例如下:

  1. interface GenericInterface<U> { 
  2.   value: U 
  3.   getIdentity: () => U 
  4.  
  5. class IdentityClass<T> implements GenericInterface<T> { 
  6.   value: T 
  7.  
  8.   constructor(value: T) { 
  9.     this.value = value 
  10.   } 
  11.  
  12.   getIdentity(): T { 
  13.     return this.value 
  14.   } 
  15.  
  16. const myNumberClass = new IdentityClass<number>(68); 
  17. console.log(myNumberClass.getIdentity()); // 68 
  18.  
  19. const myStringClass = new IdentityClass<string>("Semlinker!"); 
  20. console.log(myStringClass.getIdentity()); // Semlinker! 

接下來(lái)我們以實(shí)例化 myNumberClass 為例,來(lái)分析一下其調(diào)用過(guò)程:

  • 在實(shí)例化 IdentityClass 對(duì)象時(shí),我們傳入 number 類(lèi)型和構(gòu)造函數(shù)參數(shù)值 68;
  • 之后在 IdentityClass 類(lèi)中,類(lèi)型變量 T 的值變成 number 類(lèi)型;
  • IdentityClass 類(lèi)實(shí)現(xiàn)了 GenericInterface,而此時(shí) T 表示 number 類(lèi)型,因此等價(jià)于該類(lèi)實(shí)現(xiàn)了 GenericInterface 接口;
  • 而對(duì)于 GenericInterface 接口來(lái)說(shuō),類(lèi)型變量 U 也變成了 number。這里我有意使用不同的變量名,以表明類(lèi)型值沿鏈向上傳播,且與變量名無(wú)關(guān)。

相信看到這里一些讀者會(huì)有疑問(wèn),我們什么時(shí)候需要使用泛型呢?通常在決定是否使用泛型時(shí),我們有以下兩個(gè)參考標(biāo)準(zhǔn):

  • 當(dāng)你的函數(shù)、接口或類(lèi)將處理多種數(shù)據(jù)類(lèi)型時(shí);
  • 當(dāng)函數(shù)、接口或類(lèi)在多個(gè)地方使用該數(shù)據(jù)類(lèi)型時(shí)。

很有可能你沒(méi)有辦法保證在項(xiàng)目早期就使用泛型的組件,但是隨著項(xiàng)目的發(fā)展,組件的功能通常會(huì)被擴(kuò)展。這種增加的可擴(kuò)展性最終很可能會(huì)滿(mǎn)足上述兩個(gè)條件,在這種情況下,引入泛型將比復(fù)制組件來(lái)滿(mǎn)足一系列數(shù)據(jù)類(lèi)型更方便。

我們將在本文的后面探討更多滿(mǎn)足這兩個(gè)條件的用例。不過(guò)在這樣做之前,讓我們先介紹一下 Typescript 泛型提供的其他功能。

四、泛型約束

有時(shí)我們可能希望限制每個(gè)類(lèi)型變量接受的類(lèi)型數(shù)量,這就是泛型約束的作用。下面我們來(lái)舉幾個(gè)例子,介紹一下如何使用泛型約束。

4.1 確保屬性存在

有時(shí)候,我們希望類(lèi)型變量對(duì)應(yīng)的類(lèi)型上存在某些屬性。這時(shí),除非我們顯式地將特定屬性定義為類(lèi)型變量,否則編譯器不會(huì)知道它們的存在。

一個(gè)很好的例子是在處理字符串或數(shù)組時(shí),我們會(huì)假設(shè) length 屬性是可用的。讓我們?cè)俅问褂?identity 函數(shù)并嘗試輸出參數(shù)的長(zhǎng)度:

  1. function identity<T>(arg: T): T { 
  2.   // Property 'length' does not exist on type 'T'.(2339) 
  3.   console.log(arg.length); // Error 
  4.   return arg; 

在這種情況下,編譯器沒(méi)法確認(rèn) T 類(lèi)型一定含有 length 屬性,尤其是在可以將任何類(lèi)型賦給類(lèi)型變量 T 的情況下。我們需要做的就是讓類(lèi)型變量 extends 一個(gè)含有我們所需屬性的接口,比如這樣:

  1. interface Length { 
  2.   length: number; 
  3.  
  4. function identity<T extends Length>(arg: T): T { 
  5.   console.log(arg.length); // 可以獲取length屬性 
  6.   return arg; 

T extends Length 用于告訴編譯器,我們支持已經(jīng)實(shí)現(xiàn) Length 接口的任何類(lèi)型。之后,當(dāng)我們使用不含有 length 屬性的對(duì)象作為參數(shù)調(diào)用 identity 函數(shù)時(shí),TypeScript 會(huì)提示相關(guān)的錯(cuò)誤信息:

  1. identity(68); // Error 
  2. // Argument of type '68' is not assignable to parameter of type 'Length'.(2345) 

此外,我們還可以使用 , 號(hào)來(lái)分隔多種約束類(lèi)型,比如:。而對(duì)于上述的 length 屬性問(wèn)題來(lái)說(shuō),如果我們顯式地將變量設(shè)置為數(shù)組類(lèi)型,也可以解決該問(wèn)題,具體方式如下:

  1. function identity<T>(arg: T[]): T[] { 
  2.   console.log(arg.length);   
  3.   return arg;  

4.2 檢查對(duì)象上的鍵是否存在

泛型約束的另一個(gè)常見(jiàn)的使用場(chǎng)景就是檢查對(duì)象上的鍵是否存在。不過(guò)在看具體示例之前,我們得來(lái)了解一下 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,該操作符可以用于獲取某種類(lèi)型的所有鍵,其返回類(lèi)型是聯(lián)合類(lèi)型。 下面我們來(lái)舉個(gè) keyof 的使用示例:

  1. interface Person { 
  2.   name: string; 
  3.   age: number; 
  4.   location: string; 
  5.  
  6. type K1 = keyof Person; // "name" | "age" | "location" 
  7. type K2 = keyof Person[];  // number | "length" | "push" | "concat" | ... 
  8. type K3 = keyof { [x: string]: Person };  // string | number 

提示:TypeScript Playground v4.2.3 版本以上的編譯器不會(huì)顯示 keyof 操作符的結(jié)果

通過(guò) keyof 操作符,我們就可以獲取指定類(lèi)型的所有鍵,之后我們就可以結(jié)合前面介紹的 extends 約束,即限制輸入的屬性名包含在 keyof 返回的聯(lián)合類(lèi)型中。具體的使用方式如下:

  1. function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 
  2.   return obj[key]; 

在以上的 getProperty 函數(shù)中,我們通過(guò) K extends keyof T 確保參數(shù) key 一定是對(duì)象中含有的鍵,這樣就不會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤。這是一個(gè)類(lèi)型安全的解決方案,與簡(jiǎn)單調(diào)用 let value = obj[key]; 是不同的。

下面我們來(lái)看一下如何使用 getProperty 函數(shù):

  1. enum Difficulty { 
  2.   Easy, 
  3.   Intermediate, 
  4.   Hard 
  5.  
  6. function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { 
  7.   return obj[key]; 
  8.  
  9. let tsInfo = { 
  10.    name"Typescript"
  11.    supersetOf: "Javascript"
  12.    difficulty: Difficulty.Intermediate 
  13.   
  14. let difficulty: Difficulty =  
  15.   getProperty(tsInfo, 'difficulty'); // OK 
  16.  
  17. let supersetOf: string =  
  18.   getProperty(tsInfo, 'superset_of'); // Error 

在以上示例中,對(duì)于 getProperty(tsInfo, 'superset_of') 這個(gè)表達(dá)式,TypeScript 編譯器會(huì)提示以下錯(cuò)誤信息:

  1. Argument of type '"superset_of"' is not assignable to parameter of type '"difficulty" | "name" | "supersetOf"'

很明顯通過(guò)使用泛型約束,在編譯階段我們就可以提前發(fā)現(xiàn)錯(cuò)誤,大大提高了程序的健壯性和穩(wěn)定性。接下來(lái),我們來(lái)介紹一下泛型參數(shù)默認(rèn)類(lèi)型。

五、泛型參數(shù)默認(rèn)類(lèi)型

在 TypeScript 2.3 以后,我們可以為泛型中的類(lèi)型參數(shù)指定默認(rèn)類(lèi)型。當(dāng)使用泛型時(shí)沒(méi)有在代碼中直接指定類(lèi)型參數(shù),從實(shí)際值參數(shù)中也無(wú)法推斷出類(lèi)型時(shí),這個(gè)默認(rèn)類(lèi)型就會(huì)起作用。

泛型參數(shù)默認(rèn)類(lèi)型與普通函數(shù)默認(rèn)值類(lèi)似,對(duì)應(yīng)的語(yǔ)法很簡(jiǎn)單,即 ,對(duì)應(yīng)的使用示例如下:

  1. interface Person<T=string> { 
  2.   id: T; 
  3.  
  4. const p0: Person = { id: "lolo" }; 
  5. const p1: Person<number> = { id: 28 }; 

泛型參數(shù)的默認(rèn)類(lèi)型遵循以下規(guī)則:

  • 有默認(rèn)類(lèi)型的類(lèi)型參數(shù)被認(rèn)為是可選的。
  • 必選的類(lèi)型參數(shù)不能在可選的類(lèi)型參數(shù)后。
  • 如果類(lèi)型參數(shù)有約束,類(lèi)型參數(shù)的默認(rèn)類(lèi)型必須滿(mǎn)足這個(gè)約束。
  • 當(dāng)指定類(lèi)型實(shí)參時(shí),你只需要指定必選類(lèi)型參數(shù)的類(lèi)型實(shí)參。未指定的類(lèi)型參數(shù)會(huì)被解析為它們的默認(rèn)類(lèi)型。
  • 如果指定了默認(rèn)類(lèi)型,且類(lèi)型推斷無(wú)法選擇一個(gè)候選類(lèi)型,那么將使用默認(rèn)類(lèi)型作為推斷結(jié)果。
  • 一個(gè)被現(xiàn)有類(lèi)或接口合并的類(lèi)或者接口的聲明可以為現(xiàn)有類(lèi)型參數(shù)引入默認(rèn)類(lèi)型。
  • 一個(gè)被現(xiàn)有類(lèi)或接口合并的類(lèi)或者接口的聲明可以引入新的類(lèi)型參數(shù),只要它指定了默認(rèn)類(lèi)型。

六、泛型條件類(lèi)型

在 TypeScript 2.8 中引入了條件類(lèi)型,使得我們可以根據(jù)某些條件得到不同的類(lèi)型,這里所說(shuō)的條件是類(lèi)型兼容性約束。條件類(lèi)型會(huì)以一個(gè)條件表達(dá)式進(jìn)行類(lèi)型關(guān)系檢測(cè),從而在兩種類(lèi)型中選擇其一:

  1. T extends U ? X : Y 

以上表達(dá)式的意思是:若 T 能夠賦值給 U,那么類(lèi)型是 X,否則為 Y。在條件類(lèi)型表達(dá)式中,我們通常還會(huì)結(jié)合 infer 關(guān)鍵字,實(shí)現(xiàn)類(lèi)型抽?。?/p>

  1. interface Dictionary<T = any> { 
  2.   [key: string]: T; 
  3.   
  4. type StrDict = Dictionary<string> 
  5.  
  6. type DictMember<T> = T extends Dictionary<infer V> ? V : never 
  7. type StrDictMember = DictMember<StrDict> // string 

在上面示例中,當(dāng)類(lèi)型 T 滿(mǎn)足 T extends Dictionary 約束時(shí),我們會(huì)使用 infer 關(guān)鍵字聲明了一個(gè)類(lèi)型變量 V,并返回該類(lèi)型,否則返回 never 類(lèi)型。

在 TypeScript 中,never 類(lèi)型表示的是那些永不存在的值的類(lèi)型。例如, never 類(lèi)型是那些總是會(huì)拋出異?;蚋揪筒粫?huì)有返回值的函數(shù)表達(dá)式或箭頭函數(shù)表達(dá)式的返回值類(lèi)型。

另外,需要注意的是,沒(méi)有類(lèi)型是 never 的子類(lèi)型或可以賦值給 never 類(lèi)型(除了 never 本身之外)。即使 any 也不可以賦值給 never。

條件類(lèi)型還有一個(gè)特性:分布式條件類(lèi)型。當(dāng)檢測(cè)的類(lèi)型是由 ”裸類(lèi)型“(指該類(lèi)型未被包裝過(guò)) 組成的聯(lián)合類(lèi)型時(shí),條件類(lèi)型會(huì)被自動(dòng)分發(fā)成聯(lián)合類(lèi)型。以 T extends U ? X : Y 條件類(lèi)型為例,當(dāng)類(lèi)型參數(shù)的為 A | B | C 時(shí),該條件類(lèi)型將會(huì)被解析為 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。

分布式條件類(lèi)型的使用示例如下:

  1. type TypeName<T> = T extends string 
  2.   ? "string" 
  3.   : T extends number 
  4.   ? "number" 
  5.   : T extends boolean 
  6.   ? "boolean" 
  7.   : T extends undefined 
  8.   ? "undefined" 
  9.   : T extends Function 
  10.   ? "function" 
  11.   : "object"
  12.  
  13. type T10 = TypeName<string | (() => void)>; // "string" | "function" 
  14. type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined" 
  15. type T11 = TypeName<string[] | number[]>; // "object" 

七、泛型工具類(lèi)型

為了方便開(kāi)發(fā)者 TypeScript 內(nèi)置了一些常用的工具類(lèi)型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考慮,這里我們只簡(jiǎn)單介紹其中幾個(gè)常用的工具類(lèi)型。

7.1 Partial

Partial 的作用就是將某個(gè)類(lèi)型里的屬性全部變?yōu)榭蛇x項(xiàng) ?。

定義:

  1. type Partial<T> = { 
  2.   [P in keyof T]?: T[P]; 
  3. }; 

以上 Partial 類(lèi)型被稱(chēng)為映射類(lèi)型,用于把已有的類(lèi)型轉(zhuǎn)換成新的類(lèi)型。在以上代碼中,我們首先通過(guò) keyof T 拿到 T 的所有屬性名,然后使用 in 進(jìn)行遍歷,將值賦給類(lèi)型變量 P,最后通過(guò) T[P] 取得屬性 P 對(duì)應(yīng)的類(lèi)型。中間的 ? 號(hào),表示將屬性變?yōu)榭蛇x。

示例:

  1. interface Todo { 
  2.   title: string; 
  3.   description: string; 
  4.  
  5. function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) { 
  6.   return { ...todo, ...fieldsToUpdate }; 
  7.  
  8. const todo1 = { 
  9.   title: "Learn TS"
  10.   description: "Learn TypeScript" 
  11. }; 
  12.  
  13. const todo2 = updateTodo(todo1, { 
  14.   description: "Learn TypeScript Handbook" 
  15. }); 

在上面的 updateTodo 方法中,我們利用 Partial 工具類(lèi)型,定義 fieldsToUpdate 的類(lèi)型為 Partial,即:

  1.    title?: string | undefined; 
  2.    description?: string | undefined; 

是不是覺(jué)得 Partial 使用起來(lái)挺簡(jiǎn)單的,那么如何定義一個(gè) SetOptional 工具類(lèi)型,支持把給定的 keys 對(duì)應(yīng)的屬性變成可選的。對(duì)應(yīng)的使用示例如下所示:

  1. type Foo = { 
  2.  a: number; 
  3.  b?: string; 
  4.  c: boolean; 
  5.  
  6. // 測(cè)試用例 
  7. type SomeOptional = SetOptional<Foo, 'a' | 'b'>; 
  8.  
  9. // type SomeOptional = { 
  10. //  a?: number; // 該屬性已變成可選的 
  11. //  b?: string; // 保持不變 
  12. //  c: boolean;  
  13. // } 

7.2 Record

Record 的作用是將 K 中所有的屬性的值轉(zhuǎn)化為 T 類(lèi)型。

定義:

  1. type Record<K extends keyof any, T> = { 
  2.   [P in K]: T; 
  3. }; 

示例:

  1. interface PageInfo { 
  2.   title: string; 
  3.  
  4. type Page = "home" | "about" | "contact"
  5.  
  6. const x: Record<Page, PageInfo> = { 
  7.   about: { title: "about" }, 
  8.   contact: { title: "contact" }, 
  9.   home: { title: "home" } 
  10. }; 

7.3 Pick

Pick 的作用是將某個(gè)類(lèi)型中的子屬性挑出來(lái),變成包含這個(gè)類(lèi)型部分屬性的子類(lèi)型。

定義:

  1. type Pick<T, K extends keyof T> = { 
  2.   [P in K]: T[P]; 
  3. }; 

示例:

  1. interface Todo { 
  2.   title: string; 
  3.   description: string; 
  4.   completed: boolean; 
  5.  
  6. type TodoPreview = Pick<Todo, "title" | "completed">; 
  7.  
  8. const todo: TodoPreview = { 
  9.   title: "Learn TS"
  10.   completed: false 
  11. }; 

在掌握 Pick 的用法之后,你可以想一下,如何定義一個(gè) ConditionalPick 工具類(lèi)型,支持根據(jù)指定的 Condition 條件來(lái)生成新的類(lèi)型,對(duì)應(yīng)的使用示例如下:

  1. interface Example { 
  2.  a: string; 
  3.  b: string | number; 
  4.  c: () => void; 
  5.  d: {}; 
  6.  
  7. // 測(cè)試用例: 
  8. type StringKeysOnly = ConditionalPick<Example, string>; 
  9. //=> { a: string } 

7.4 Exclude

Exclude 的作用是將某個(gè)類(lèi)型中屬于另一個(gè)的類(lèi)型移除掉。

定義:

  1. type Exclude<T, U> = T extends U ? never : T; 

如果 T 能賦值給 U 類(lèi)型的話(huà),那么就會(huì)返回 never 類(lèi)型,否則返回 T 類(lèi)型。最終實(shí)現(xiàn)的效果就是將 T 中某些屬于 U 的類(lèi)型移除掉。

示例:

  1. type T0 = Exclude<"a" | "b" | "c""a">; // "b" | "c" 
  2. type T1 = Exclude<"a" | "b" | "c""a" | "b">; // "c" 
  3. type T2 = Exclude<string | number | (() => void), Function>; // string | number 

由以上結(jié)果可知,Exclude 工具類(lèi)型利用了前面介紹的分布式條件類(lèi)型的特性。

7.5 ReturnType

ReturnType 的作用是用于獲取函數(shù) T 的返回類(lèi)型。

定義:

  1. type ReturnType any> = T extends (...args: any) => infer R ? R : any

示例:

  1. type T0 = ReturnType<() => string>; // string 
  2. type T1 = ReturnType<(s: string) => void>; // void 
  3. type T2 = ReturnType<<T>() => T>; // {} 
  4. type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[] 
  5. type T4 = ReturnType<any>; // any 
  6. type T5 = ReturnType<never>; // any 
  7. type T6 = ReturnType<string>; // Error 
  8. type T7 = ReturnType<Function>; // Error 

簡(jiǎn)單介紹了泛型工具類(lèi)型,最后我們來(lái)介紹如何使用泛型來(lái)創(chuàng)建對(duì)象。

八、使用泛型創(chuàng)建對(duì)象

8.1 構(gòu)造簽名

有時(shí),泛型類(lèi)可能需要基于傳入的泛型 T 來(lái)創(chuàng)建其類(lèi)型相關(guān)的對(duì)象。比如:

  1. class FirstClass { 
  2.   id: number | undefined; 
  3.  
  4. class SecondClass { 
  5.   name: string | undefined; 
  6.  
  7. class GenericCreator<T> { 
  8.   create(): T { 
  9.     return new T(); 
  10.   } 
  11.  
  12. const creator1 = new GenericCreator<FirstClass>(); 
  13. const firstClass: FirstClass = creator1.create(); 
  14.  
  15. const creator2 = new GenericCreator<SecondClass>(); 
  16. const secondClass: SecondClass = creator2.create(); 

在以上代碼中,我們定義了兩個(gè)普通類(lèi)和一個(gè)泛型類(lèi) GenericCreator。在通用的 GenericCreator 泛型類(lèi)中,我們定義了一個(gè)名為 create 的成員方法,該方法會(huì)使用 new 關(guān)鍵字來(lái)調(diào)用傳入的實(shí)際類(lèi)型的構(gòu)造函數(shù),來(lái)創(chuàng)建對(duì)應(yīng)的對(duì)象。但可惜的是,以上代碼并不能正常運(yùn)行,對(duì)于以上代碼,在 TypeScript v4.4.3 編譯器下會(huì)提示以下錯(cuò)誤:

  1. 'T' only refers to a type, but is being used as a value here. 

這個(gè)錯(cuò)誤的意思是:T 類(lèi)型僅指類(lèi)型,但此處被用作值。那么如何解決這個(gè)問(wèn)題呢?根據(jù) TypeScript 文檔,為了使通用類(lèi)能夠創(chuàng)建 T 類(lèi)型的對(duì)象,我們需要通過(guò)其構(gòu)造函數(shù)來(lái)引用 T 類(lèi)型。對(duì)于上述問(wèn)題,在介紹具體的解決方案前,我們先來(lái)介紹一下構(gòu)造簽名。

在 TypeScript 接口中,你可以使用 new 關(guān)鍵字來(lái)描述一個(gè)構(gòu)造函數(shù):

  1. interface Point { 
  2.   new (x: number, y: number): Point; 

以上接口中的 new (x: number, y: number) 我們稱(chēng)之為構(gòu)造簽名,其語(yǔ)法如下:

ConstructSignature:new?TypeParametersopt?(?ParameterListopt?)?TypeAnnotationopt

在上述的構(gòu)造簽名中,TypeParametersopt 、ParameterListopt 和 TypeAnnotationopt 分別表示:可選的類(lèi)型參數(shù)、可選的參數(shù)列表和可選的類(lèi)型注解。與該語(yǔ)法相對(duì)應(yīng)的幾種常見(jiàn)的使用形式如下:

  1. new C   
  2. new C ( ... )   
  3. new C < ... > ( ... ) 

介紹完構(gòu)造簽名,我們?cè)賮?lái)介紹一個(gè)與之相關(guān)的概念,即構(gòu)造函數(shù)類(lèi)型。

8.2 構(gòu)造函數(shù)類(lèi)型

在 TypeScript 語(yǔ)言規(guī)范中這樣定義構(gòu)造函數(shù)類(lèi)型:

An object type containing one or more construct signatures is said to be a constructor type. Constructor types may be written using constructor type literals or by including construct signatures in object type literals.

通過(guò)規(guī)范中的描述信息,我們可以得出以下結(jié)論:

  • 包含一個(gè)或多個(gè)構(gòu)造簽名的對(duì)象類(lèi)型被稱(chēng)為構(gòu)造函數(shù)類(lèi)型;
  • 構(gòu)造函數(shù)類(lèi)型可以使用構(gòu)造函數(shù)類(lèi)型字面量或包含構(gòu)造簽名的對(duì)象類(lèi)型字面量來(lái)編寫(xiě)。

那么什么是構(gòu)造函數(shù)類(lèi)型字面量呢?構(gòu)造函數(shù)類(lèi)型字面量是包含單個(gè)構(gòu)造函數(shù)簽名的對(duì)象類(lèi)型的簡(jiǎn)寫(xiě)。具體來(lái)說(shuō),構(gòu)造函數(shù)類(lèi)型字面量的形式如下:

  1. new < T1, T2, ... > ( p1, p2, ... ) => R 

該形式與以下對(duì)象類(lèi)型字面量是等價(jià)的:

  1. { new < T1, T2, ... > ( p1, p2, ... ) : R } 

下面我們來(lái)舉個(gè)實(shí)際的示例:

  1. // 構(gòu)造函數(shù)類(lèi)型字面量 
  2.  
  3. new (x: number, y: number) => Point 

等價(jià)于以下對(duì)象類(lèi)型字面量:

  1.    new (x: number, y: number): Point; 

8.3 構(gòu)造函數(shù)類(lèi)型的應(yīng)用

在介紹構(gòu)造函數(shù)類(lèi)型的應(yīng)用前,我們先來(lái)看個(gè)例子:

  1. interface Point { 
  2.   new (x: number, y: number): Point; 
  3.   x: number; 
  4.   y: number; 
  5.  
  6. class Point2D implements Point { 
  7.   readonly x: number; 
  8.   readonly y: number; 
  9.  
  10.   constructor(x: number, y: number) { 
  11.     this.x = x; 
  12.     this.y = y; 
  13.   } 
  14.  
  15. const point: Point = new Point2D(1, 2); 

對(duì)于以上的代碼,TypeScript 編譯器會(huì)提示以下錯(cuò)誤信息:

  1. Class 'Point2D' incorrectly implements interface 'Point'
  2. Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'

相信很多剛接觸 TypeScript 不久的小伙伴都會(huì)遇到上述的問(wèn)題。要解決這個(gè)問(wèn)題,我們就需要把對(duì)前面定義的 Point 接口進(jìn)行分離,即把接口的屬性和構(gòu)造函數(shù)類(lèi)型進(jìn)行分離:

  1. interface Point { 
  2.   x: number; 
  3.   y: number; 
  4.  
  5. interface PointConstructor { 
  6.   new (x: number, y: number): Point; 

完成接口拆分之后,除了前面已經(jīng)定義的 Point2D 類(lèi)之外,我們又定義了一個(gè) newPoint 工廠(chǎng)函數(shù),該函數(shù)用于根據(jù)傳入的 PointConstructor 類(lèi)型的構(gòu)造函數(shù),來(lái)創(chuàng)建對(duì)應(yīng)的 Point 對(duì)象。

  1. class Point2D implements Point { 
  2.   readonly x: number; 
  3.   readonly y: number; 
  4.  
  5.   constructor(x: number, y: number) { 
  6.     this.x = x; 
  7.     this.y = y; 
  8.   } 
  9.  
  10. function newPoint( 
  11.   pointConstructor: PointConstructor, 
  12.   x: number, 
  13.   y: number 
  14. ): Point { 
  15.   return new pointConstructor(x, y); 
  16.  
  17. const point: Point = newPoint(Point2D, 1, 2); 

8.4 使用泛型創(chuàng)建對(duì)象

了解完構(gòu)造簽名和構(gòu)造函數(shù)類(lèi)型之后,下面我們來(lái)開(kāi)始解決上面遇到的問(wèn)題,首先我們需要重構(gòu)一下 create 方法,具體如下所示:

  1. class GenericCreator<T> { 
  2.   create<T>(c: { new (): T }): T { 
  3.     return new c(); 
  4.   } 

在以上代碼中,我們重新定義了 create 成員方法,根據(jù)該方法的簽名,我們可以知道該方法接收一個(gè)參數(shù),其類(lèi)型是構(gòu)造函數(shù)類(lèi)型,且該構(gòu)造函數(shù)不包含任何參數(shù),調(diào)用該構(gòu)造函數(shù)后,會(huì)返回類(lèi)型 T 的實(shí)例。

如果構(gòu)造函數(shù)含有參數(shù)的話(huà),比如包含一個(gè) number 類(lèi)型的參數(shù)時(shí),我們可以這樣定義 create 方法:

  1. create<T>(c: { new(a: number): T; }, num: number): T { 
  2.   return new c(num); 

更新完 GenericCreator 泛型類(lèi),我們就可以使用下面的方式來(lái)創(chuàng)建 FirstClass 和 SecondClass 類(lèi)的實(shí)例:

  1. const creator1 = new GenericCreator<FirstClass>(); 
  2. const firstClass: FirstClass = creator1.create(FirstClass); 
  3.  
  4. const creator2 = new GenericCreator<SecondClass>(); 
  5. const secondClass: SecondClass = creator2.create(SecondClass); 

8.5 抽象構(gòu)造簽名

在 TypeScript 4.2 版本中引入了抽象構(gòu)造簽名,用于解決以下的問(wèn)題:

  1. type ConstructorFunction = new (...args: any[]) => any
  2.  
  3. abstract class Utilities {} 
  4.  
  5. // Type 'typeof Utilities' is not assignable to type 'ConstructorFunction'
  6. // Cannot assign an abstract constructor type to a non-abstract constructor type. 
  7. let UtilityClass: ConstructorFunction = Utilities; // Error. 

由以上的錯(cuò)誤信息可知,我們不能把抽象構(gòu)造器類(lèi)型分配給非抽象的構(gòu)造器類(lèi)型。針對(duì)這個(gè)問(wèn)題,我們需要使用 abstract 修飾符:

  1. declare type ConstructorFunction = abstract new (...args: any[]) => any

需要注意的是,對(duì)于抽象構(gòu)造器類(lèi)型,我們也可以傳入具體的實(shí)現(xiàn)類(lèi):

  1. declare type ConstructorFunction = abstract new (...args: any[]) => any
  2.  
  3. abstract class Utilities {} 
  4. class UtilitiesConcrete extends Utilities {} 
  5.  
  6. let UtilityClass: ConstructorFunction = Utilities; // Ok 
  7. let UtilityClass1: ConstructorFunction = UtilitiesConcrete; // Ok 

而對(duì)于 TypeScript 4.2 以下的版本,我們可以通過(guò)以下方式來(lái)解決上面的問(wèn)題:

  1. type Constructor<T> = Function & { prototype: T } 
  2.  
  3. abstract class Utilities {} 
  4.  
  5. class UtilitiesConcrete extends Utilities {} 
  6.  
  7. let UtilityClass: Constructor<Utilities> = Utilities; 
  8. let UtilityClass1: Constructor<UtilitiesConcrete> = UtilitiesConcrete; 

九、可變?cè)M類(lèi)型

在 TypeScript 4.0 版本支持可變?cè)M類(lèi)型,其中有兩個(gè)新的變化。第一個(gè)變化是元組類(lèi)型的展開(kāi)運(yùn)算可以支持泛型了:

  1. function tail<T extends any[]>(arr: readonly [any, ...T]) { 
  2.   const [_ignored, ...rest] = arr; 
  3.   return rest; 
  4.  
  5. const myTuple = [1, 2, 3, 4] as const; 
  6. const myArray = ["hello""world"]; 
  7.   
  8. const r1 = tail(myTuple); // r1: [2, 3, 4] 
  9. const r2 = tail([...myTuple, ...myArray] as const); // r2: [2, 3, 4, ...string[]] 

第二個(gè)變化是 rest 元素可以出現(xiàn)在元組中的任何位置,而不僅僅是在結(jié)尾!

  1. type Strings = [string, string]; 
  2. type Numbers = [number, number]; 
  3.  
  4. type StrStrNumNumBool = [...Strings, ...Numbers, boolean]; 

對(duì)于 TypeScript 4.0 以下的版本,以上代碼將會(huì)出現(xiàn)以下的錯(cuò)誤信息:

  1. A rest element must be last in a tuple type.(1256) 

利用這兩個(gè)特性,我們就可以實(shí)現(xiàn)一個(gè)類(lèi)型良好的 concat 函數(shù):

  1. type Arr = readonly any[]; 
  2.   
  3. function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] { 
  4.   return [...arr1, ...arr2]; 
  5.  
  6. const arr3 = concat([1, 2, 3], ["a""b""c"]) 

關(guān)于可變?cè)M類(lèi)型的相關(guān)內(nèi)容,就不展開(kāi)介紹了,感興趣的小伙伴可以自行閱讀 TypeScript 4.0 的相關(guān)文檔。

十、泛型是如何工作的

最后,阿寶哥將使用 ts-ast-viewer 在線(xiàn)工具,帶大家換個(gè)角度來(lái)學(xué)習(xí) TypeScript 的泛型。對(duì)應(yīng)的示例代碼如下:

  1. type Head<T extends Array<any>> = T extends [any, ...any] ? T[0] : never 
  2.  
  3. type H0 = Head<[1, 2, 3]> // 1 

10.1 類(lèi)型變量 AST

10.2 條件類(lèi)型 AST

10.3 類(lèi)型引用 AST

所使用在線(xiàn)工具的地址為:https://ts-ast-viewer.com/

建議大家實(shí)際使用一下 ts-ast-viewer 這個(gè)在線(xiàn)工具,詳細(xì)看一下生成的節(jié)點(diǎn),這樣的話(huà),可以讓你更好地理解 TypeScript 的泛型。

 

責(zé)任編輯:武曉燕 來(lái)源: 全棧修仙之路
相關(guān)推薦

2020-09-15 08:35:57

TypeScript JavaScript類(lèi)型

2020-06-12 09:20:33

前端Blob字符串

2020-07-28 08:26:34

WebSocket瀏覽器

2010-08-23 09:56:09

Java性能監(jiān)控

2011-09-15 17:10:41

2021-02-01 23:23:39

FiddlerCharlesWeb

2009-12-10 09:37:43

2022-10-13 11:48:37

Web共享機(jī)制操作系統(tǒng)

2021-07-05 05:34:10

Typescript語(yǔ)言開(kāi)發(fā)

2020-08-11 11:20:49

Linux命令使用技巧

2021-12-22 09:08:39

JSON.stringJavaScript字符串

2015-06-19 13:54:49

2021-12-29 11:38:59

JS前端沙箱

2012-11-23 10:57:44

Shell

2022-11-04 08:19:18

gRPC框架項(xiàng)目

2018-12-06 09:12:58

2017-12-15 13:44:22

2021-11-16 08:51:29

Node JavaScript變量類(lèi)型

2010-04-10 13:06:24

Windows Emb

2022-12-07 08:16:50

Vue 3技巧數(shù)組
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)