想要學習更多前端內容,文末有我們的前端學習交流群。(預計閱讀全文需10分鐘)
覺得 TypeScript 泛型有點難,想系統學習 TypeScript 泛型相關知識的小伙伴們看過來,本文從八個方面入手,全方位帶你一步步學習 TypeScript 中泛型,詳細的內容大綱請看下圖:
一、泛型是什么
軟件工程中,我們不僅要創建一致的定義良好的 API,同時也要考慮可重用性。 組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語言中,可以使用泛型來創建可重用的組件,一個組件可以支持多種類型的數據。 這樣用戶就可以以自己的數據類型來使用組件。
設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員可以是:類的實例成員、類的方法、函數參數和函數返回值。
為了便于大家更好地理解上述的內容,我們來舉個例子,在這個例子中,我們將一步步揭示泛型的作用。首先我們來定義一個通用的 identity 函數,該函數接收一個參數并直接返回它:
function?identity?(value)?{?return?value;}console.log(identity(1))?//?1
現在,我們將 identity 函數做適當的調整,以支持 TypeScript 的 Number 類型的參數:
function?identity?(value:?Number)?:?Number?{?return?value;}console.log(identity(1))?//?1
這里 identity 的問題是我們將 Number 類型分配給參數和返回類型,使該函數僅可用于該原始類型。但該函數并不是可擴展或通用的,很明顯這并不是我們所希望的。
我們確實可以把 Number 換成 any,我們失去了定義應該返回哪種類型的能力,并且在這個過程中使編譯器失去了類型保護的作用。我們的目標是讓 identity 函數可以適用于任何特定的類型,為了實現這個目標,我們可以使用泛型來解決這個問題,具體實現方式如下:
function?identity?<T>(value:?T)?:?T?{?return?value;}console.log(identity<Number>(1))?//?1
對于剛接觸 TypeScript 泛型的讀者來說,首次看到 <T> 語法會感到陌生。但這沒什么可擔心的,就像傳遞參數一樣,我們傳遞了我們想要用于特定函數調用的類型。
參考上面的圖片,當我們調用 identity<Number>(1) ,Number 類型就像參數 1 一樣,它將在出現 T 的任何位置填充該類型。圖中 <T> 內部的 T 被稱為類型變量,它是我們希望傳遞給 identity 函數的類型占位符,同時它被分配給 value 參數用來代替它的類型:此時 T 充當的是類型,而不是特定的 Number 類型。
其中 T 代表 Type,在定義泛型時通常用作第一個類型變量名稱。但實際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:
K(Key):表示對象中的鍵類型;
V(Value):表示對象中的值類型;
E(Element):表示元素類型。
其實并不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 U,用于擴展我們定義的 identity 函數:
function?identity?<T,?U>(value:?T,?message:?U)?:?T?{?console.log(message);?return?value;}console.log(identity<Number,?string>(68,?"Semlinker"));
除了為類型變量顯式設定值之外,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔。我們可以完全省略尖括號,比如:
function?identity?<T,?U>(value:?T,?message:?U)?:?T?{?console.log(message);?return?value;}console.log(identity(68,?"Semlinker"));
對于上述代碼,編譯器足夠聰明,能夠知道我們的參數類型,并將它們賦值給 T 和 U,而不需要開發人員顯式指定它們。下面我們來看張動圖,直觀地感受一下類型傳遞的過程:
注意:動圖應該是 —— identity<Number[]>([1,2,3])
如你所見,該函數接收你傳遞給它的任何類型,使得我們可以為不同類型創建可重用的組件。現在我們再來看一下 identity 函數:
function?identity?<T,?U>(value:?T,?message:?U)?:?T?{?console.log(message);?return?value;}
相比之前定義的 identity 函數,新的 identity 函數增加了一個類型變量 U,但該函數的返回類型我們仍然使用 T。如果我們想要返回兩種類型的對象該怎么辦呢?針對這個問題,我們有多種方案,其中一種就是使用元組,即為元組設置通用的類型:
function?identity?<T,?U>(value:?T,?message:?U)?:?[T,?U]?{??return?[value,?message];}
雖然使用元組解決了上述的問題,但有沒有其它更好的方案呢?答案是有的,你可以使用泛型接口。
二、泛型接口
為了解決上面提到的問題,首先讓我們創建一個用于的 identity 函數通用 Identities 接口:
interface?Identities<V,?M>?{?value:?V,?message:?M}
在上述的 Identities 接口中,我們引入了類型變量 V 和 M,來進一步說明有效的字母都可以用于表示類型變量,之后我們就可以將 Identities 接口作為 identity 函數的返回類型:
function?identity<T,?U>?(value:?T,?message:?U):?Identities<T,?U>?{?console.log(value?+?":?"?+?typeof?(value));?console.log(message?+?":?"?+?typeof?(message));?let?identities:?Identities<T,?U>?=?{????value,????message??};?return?identities;}console.log(identity(68,?"Semlinker"));
以上代碼成功運行后,在控制臺會輸出以下結果:
68:?numberSemlinker:?string{value:?68,?message:?"Semlinker"}
泛型除了可以應用在函數和接口之外,它也可以應用在類中,下面我們就來看一下在類中如何使用泛型。
三、泛型類
在類中使用泛型也很簡單,我們只需要在類名后面,使用 <T, ...> 的語法定義任意多個類型變量,具體示例如下:
interface?GenericInterface<U>?{?value:?U?getIdentity:?()?=>?U}class?IdentityClass<T>?implements?GenericInterface<T>?{?value:?T?constructor(value:?T)?{?this.value?=?value??}?getIdentity():?T?{?return?this.value??}}const?myNumberClass?=?new?IdentityClass<Number>(68);console.log(myNumberClass.getIdentity());?//?68const?myStringClass?=?new?IdentityClass<string>("Semlinker!");console.log(myStringClass.getIdentity());?//?Semlinker!
接下來我們以實例化 myNumberClass 為例,來分析一下其調用過程:
在實例化 IdentityClass 對象時,我們傳入 Number 類型和構造函數參數值 68;
之后在 IdentityClass 類中,類型變量 T 的值變成 Number 類型;
IdentityClass 類實現了 GenericInterface<T>,而此時 T 表示 Number 類型,因此等價于該類實現了 GenericInterface<Number> 接口;
而對于 GenericInterface<U> 接口來說,類型變量 U 也變成了 Number。這里我有意使用不同的變量名,以表明類型值沿鏈向上傳播,且與變量名無關。
泛型類可確保在整個類中一致地使用指定的數據類型。比如,你可能已經注意到在使用 Typescript 的 React 項目中使用了以下約定:
type?Props?=?{??className?:?string???...};type?State?=?{??submitted?:?bool???...};class?MyComponent?extends?React.Component<Props,?State>?{???...}
在以上代碼中,我們將泛型與 React 組件一起使用,以確保組件的 props 和 state 是類型安全的。
相信看到這里一些讀者會有疑問,我們在什么時候需要使用泛型呢?通常在決定是否使用泛型時,我們有以下兩個參考標準:
當你的函數、接口或類將處理多種數據類型時;
當函數、接口或類在多個地方使用該數據類型時。
很有可能你沒有辦法保證在項目早期就使用泛型的組件,但是隨著項目的發展,組件的功能通常會被擴展。這種增加的可擴展性最終很可能會滿足上述兩個條件,在這種情況下,引入泛型將比復制組件來滿足一系列數據類型更干凈。
我們將在本文的后面探討更多滿足這兩個條件的用例。不過在這樣做之前,讓我們先介紹一下 Typescript 泛型提供的其他功能。
四、泛型約束
有時我們可能希望限制每個類型變量接受的類型數量,這就是泛型約束的作用。下面我們來舉幾個例子,介紹一下如何使用泛型約束。
4.1 確保屬性存在
有時候,我們希望類型變量對應的類型上存在某些屬性。這時,除非我們顯式地將特定屬性定義為類型變量,否則編譯器不會知道它們的存在。
一個很好的例子是在處理字符串或數組時,我們會假設 length 屬性是可用的。讓我們再次使用 identity 函數并嘗試輸出參數的長度:
function?identity<T>(arg:?T):?T?{?console.log(arg.length);?//?Error?return?arg;}
在這種情況下,編譯器將不會知道 T 確實含有 length 屬性,尤其是在可以將任何類型賦給類型變量 T 的情況下。我們需要做的就是讓類型變量 extends 一個含有我們所需屬性的接口,比如這樣:
interface?Length?{?length:?number;}function?identity<T?extends?Length>(arg:?T):?T?{?console.log(arg.length);?//?可以獲取length屬性?return?arg;}
T extends Length 用于告訴編譯器,我們支持已經實現 Length 接口的任何類型。之后,當我們使用不含有 length 屬性的對象作為參數調用 ?identity 函數時,TypeScript 會提示相關的錯誤信息:
identity(68);?//?Error//?Argument?of?type?'68'?is?not?assignable?to?parameter?of?type?'Length'.(2345)
此外,我們還可以使用 , 號來分隔多種約束類型,比如:<T extends Length, Type2, Type3>。而對于上述的 length 屬性問題來說,如果我們顯式地將變量設置為數組類型,也可以解決該問題,具體方式如下:
function?identity<T>(arg:?T[]):?T[]?{???console.log(arg.length);?????return?arg;?}//?orfunction?identity<T>(arg:?Array<T>):?Array<T>?{????????console.log(arg.length);??return?arg;?}
4.2 檢查對象上的鍵是否存在
泛型約束的另一個常見的使用場景就是檢查對象上的鍵是否存在。不過在看具體示例之前,我們得來了解一下 keyof 操作符,keyof 操作符是在 TypeScript 2.1 版本引入的,該操作符可以用于獲取某種類型的所有鍵,其返回類型是聯合類型。"耳聽為虛,眼見為實",我們來舉個 keyof 的使用示例:
interface?Person?{?name:?string;??age:?number;??location:?string;}type?K1?=?keyof?Person;?//?"name"?|?"age"?|?"location"type?K2?=?keyof?Person[];??//?number?|?"length"?|?"push"?|?"concat"?|?...type?K3?=?keyof?{?[x:?string]:?Person?};??//?string?|?number
通過 keyof 操作符,我們就可以獲取指定類型的所有鍵,之后我們就可以結合前面介紹的 extends 約束,即限制輸入的屬性名包含在 keyof 返回的聯合類型中。具體的使用方式如下:
function?getProperty<T,?K?extends?keyof?T>(obj:?T,?key:?K):?T[K]?{??return?obj[key];}
在以上的 getProperty 函數中,我們通過 K extends keyof T 確保參數 key 一定是對象中含有的鍵,這樣就不會發生運行時錯誤。這是一個類型安全的解決方案,與簡單調用 let value = obj[key]; 不同。
下面我們來看一下如何使用 getProperty 函數:
enum?Difficulty?{??Easy,??Intermediate,??Hard}function?getProperty<T,?K?extends?keyof?T>(obj:?T,?key:?K):?T[K]?{??return?obj[key];}let?tsInfo?=?{???name:?"Typescript",???supersetOf:?"Javascript",???difficulty:?Difficulty.Intermediate}?let?difficulty:?Difficulty?=???getProperty(tsInfo,?'difficulty');?//?OKlet?supersetOf:?string?=???getProperty(tsInfo,?'superset_of');?//?Error
在以上示例中,對于 getProperty(tsInfo, 'superset_of') 這個表達式,TypeScript 編譯器會提示以下錯誤信息:
Argument?of?type?'"superset_of"'?is?not?assignable?to?parameter?of?type?'"difficulty"?|?"name"?|?"supersetOf"'.(2345)
很明顯通過使用泛型約束,在編譯階段我們就可以提前發現錯誤,大大提高了程序的健壯性和穩定性。接下來,我們來介紹一下泛型參數默認類型。
五、泛型參數默認類型
在 TypeScript 2.3 以后,我們可以為泛型中的類型參數指定默認類型。當使用泛型時沒有在代碼中直接指定類型參數,從實際值參數中也無法推斷出類型時,這個默認類型就會起作用。
泛型參數默認類型與普通函數默認值類似,對應的語法很簡單,即 <T=Default Type>,對應的使用示例如下:
interface?A<T=string>?{?name:?T;}const?strA:?A?=?{?name:?"Semlinker"?};const?numB:?A<number>?=?{?name:?101?};
泛型參數的默認類型遵循以下規則:
有默認類型的類型參數被認為是可選的。
必選的類型參數不能在可選的類型參數后。
如果類型參數有約束,類型參數的默認類型必須滿足這個約束。
當指定類型實參時,你只需要指定必選類型參數的類型實參。 未指定的類型參數會被解析為它們的默認類型。
如果指定了默認類型,且類型推斷無法選擇一個候選類型,那么將使用默認類型作為推斷結果。
一個被現有類或接口合并的類或者接口的聲明可以為現有類型參數引入默認類型。
一個被現有類或接口合并的類或者接口的聲明可以引入新的類型參數,只要它指定了默認類型。
六、泛型條件類型
在 TypeScript 2.8 中引入了條件類型,使得我們可以根據某些條件得到不同的類型,這里所說的條件是類型兼容性約束。盡管以上代碼中使用了 extends 關鍵字,也不一定要強制滿足繼承關系,而是檢查是否滿足結構兼容性。
條件類型會以一個條件表達式進行類型關系檢測,從而在兩種類型中選擇其一:
T?extends?U???X?:?Y
以上表達式的意思是:若 T 能夠賦值給 U,那么類型是 X,否則為 Y。在條件類型表達式中,我們通常還會結合 infer 關鍵字,實現類型抽取:
interface?Dictionary<T?=?any>?{??[key:?string]:?T;}?type?StrDict?=?Dictionary<string>type?DictMember<T>?=?T?extends?Dictionary<infer?V>???V?:?nevertype?StrDictMember?=?DictMember<StrDict>?//?string
在上面示例中,當類型 T 滿足 T extends Dictionary 約束時,我們會使用 infer 關鍵字聲明了一個類型變量 V,并返回該類型,否則返回 never 類型。
在 TypeScript 中,never 類型表示的是那些永不存在的值的類型。 例如, never 類型是那些總是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型。
另外,需要注意的是,沒有類型是 never 的子類型或可以賦值給 never 類型(除了 never 本身之外)。 即使 any 也不可以賦值給 never。
除了上述的應用外,利用條件類型和 infer 關鍵字,我們還可以方便地實現獲取 Promise 對象的返回值類型,比如:
async?function?stringPromise()?{?return?"Hello,?Semlinker!";}interface?Person?{?name:?string;??age:?number;}async?function?personPromise()?{?return?{?name:?"Semlinker",?age:?30?}?as?Person;}type?PromiseType<T>?=?(args:?any[])?=>?Promise<T>;type?UnPromisify<T>?=?T?extends?PromiseType<infer?U>???U?:?never;type?extractStringPromise?=?UnPromisify<typeof?stringPromise>;?//?stringtype?extractPersonPromise?=?UnPromisify<typeof?personPromise>;?//?Person
七、泛型工具類型
為了方便開發者 TypeScript 內置了一些常用的工具類型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考慮,這里我們只簡單介紹其中幾個常用的工具類型。
7.1 Partial
Partial<T> 的作用就是將某個類型里的屬性全部變為可選項 ?。
定義:
/**?*?node_modules/typescript/lib/lib.es5.d.ts?*?Make?all?properties?in?T?optional?*/type?Partial<T>?=?{????[P?in?keyof?T]?:?T[P];};
在以上代碼中,首先通過 keyof T 拿到 T 的所有屬性名,然后使用 in 進行遍歷,將值賦給 P,最后通過 T[P] 取得相應的屬性值。中間的 ? 號,用于將所有屬性變為可選。
示例:
interface?Todo?{?title:?string;??description:?string;}function?updateTodo(todo:?Todo,?fieldsToUpdate:?Partial<Todo>)?{?return?{?...todo,?...fieldsToUpdate?};}const?todo1?=?{?title:?"organize?desk",?description:?"clear?clutter"};const?todo2?=?updateTodo(todo1,?{?description:?"throw?out?trash"});
在上面的 updateTodo 方法中,我們利用 Partial<T> 工具類型,定義 fieldsToUpdate 的類型為 Partial<Todo>,即:
{???title?:?string?|?undefined;???description?:?string?|?undefined;}
7.2 Record
Record<K extends keyof any, T> 的作用是將 K 中所有的屬性的值轉化為 T 類型。
定義:
/**?*?node_modules/typescript/lib/lib.es5.d.ts?*?Construct?a?type?with?a?set?of?properties?K?of?type?T?*/type?Record<K?extends?keyof?any,?T>?=?{????[P?in?K]:?T;};
示例:
interface?PageInfo?{?title:?string;}type?Page?=?"home"?|?"about"?|?"contact";const?x:?Record<Page,?PageInfo>?=?{?about:?{?title:?"about"?},?contact:?{?title:?"contact"?},?home:?{?title:?"home"?}};
7.3 Pick
Pick<T, K extends keyof T> 的作用是將某個類型中的子屬性挑出來,變成包含這個類型部分屬性的子類型。
定義:
//?node_modules/typescript/lib/lib.es5.d.ts/**?*?From?T,?pick?a?set?of?properties?whose?keys?are?in?the?union?K?*/type?Pick<T,?K?extends?keyof?T>?=?{????[P?in?K]:?T[P];};
示例:
interface?Todo?{?title:?string;??description:?string;??completed:?boolean;}type?TodoPreview?=?Pick<Todo,?"title"?|?"completed">;const?todo:?TodoPreview?=?{?title:?"Clean?room",?completed:?false};
7.4 Exclude
Exclude<T, U> 的作用是將某個類型中屬于另一個的類型移除掉。
定義:
//?node_modules/typescript/lib/lib.es5.d.ts/**?*?Exclude?from?T?those?types?that?are?assignable?to?U?*/type?Exclude<T,?U>?=?T?extends?U???never?:?T;
如果 T 能賦值給 U 類型的話,那么就會返回 never 類型,否則返回 T 類型。最終實現的效果就是將 T 中某些屬于 U 的類型移除掉。
示例:
type?T0?=?Exclude<"a"?|?"b"?|?"c",?"a">;?//?"b"?|?"c"type?T1?=?Exclude<"a"?|?"b"?|?"c",?"a"?|?"b">;?//?"c"type?T2?=?Exclude<string?|?number?|?(()?=>?void),?Function>;?//?string?|?number
7.5 ReturnType
ReturnType<T> 的作用是用于獲取函數 T 的返回類型。
定義:
//?node_modules/typescript/lib/lib.es5.d.ts/**?*?Obtain?the?return?type?of?a?function?type?*/type?ReturnType<T?extends?(...args:?any)?=>?any>?=?T?extends?(...args:?any)?=>?infer?R???R?:?any;
示例:
type?T0?=?ReturnType<()?=>?string>;?//?stringtype?T1?=?ReturnType<(s:?string)?=>?void>;?//?voidtype?T2?=?ReturnType<<T>()?=>?T>;?//?{}type?T3?=?ReturnType<<T?extends?U,?U?extends?number[]>()?=>?T>;?//?number[]type?T4?=?ReturnType<any>;?//?anytype?T5?=?ReturnType<never>;?//?anytype?T6?=?ReturnType<string>;?//?Errortype?T7?=?ReturnType<Function>;?//?Error
簡單介紹了泛型工具類型,最后我們來介紹如何使用泛型來創建對象。
八、使用泛型創建對象
8.1 構造簽名
有時,泛型類可能需要基于傳入的泛型 T 來創建其類型相關的對象。比如:
class?FirstClass?{?id:?number?|?undefined;}class?SecondClass?{?name:?string?|?undefined;}class?GenericCreator<T>?{??create():?T?{?return?new?T();??}}const?creator1?=?new?GenericCreator<FirstClass>();const?firstClass:?FirstClass?=?creator1.create();const?creator2?=?new?GenericCreator<SecondClass>();const?secondClass:?SecondClass?=?creator2.create();
在以上代碼中,我們定義了兩個普通類和一個泛型類 GenericCreator<T> 。在通用的 GenericCreator 泛型類中,我們定義了一個名為 create 的成員方法,該方法會使用 new 關鍵字來調用傳入的實際類型的構造函數,來創建對應的對象。但可惜的是,以上代碼并不能正常運行,對于以上代碼,在 TypeScript v3.9.2 編譯器下會提示以下錯誤:
'T'?only?refers?to?a?type,?but?is?being?used?as?a?value?here.
這個錯誤的意思是:T 類型僅指類型,但此處被用作值。那么如何解決這個問題呢?根據 TypeScript 文檔,為了使通用類能夠創建 T 類型的對象,我們需要通過其構造函數來引用 T 類型。對于上述問題,在介紹具體的解決方案前,我們先來介紹一下構造簽名。
在 TypeScript 接口中,你可以使用 new 關鍵字來描述一個構造函數:
interface?Point?{?new?(x:?number,?y:?number):?Point;}
以上接口中的 new (x: number, y: number) 我們稱之為構造簽名,其語法如下:
ConstructSignature:
?new?TypeParametersopt?(?*ParameterListopt*?)?TypeAnnotationopt
在上述的構造簽名中,TypeParametersopt 、ParameterListopt 和 TypeAnnotationopt 分別表示:可選的類型參數、可選的參數列表和可選的類型注解。與該語法相對應的幾種常見的使用形式如下:
new?C??new?C?(?...?)??new?C?<?...?>?(?...?)
介紹完構造簽名,我們再來介紹一個與之相關的概念,即構造函數類型。
8.2 構造函數類型
在 TypeScript 語言規范中這樣定義構造函數類型:
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.
通過規范中的描述信息,我們可以得出以下結論:
包含一個或多個構造簽名的對象類型被稱為構造函數類型;
構造函數類型可以使用構造函數類型字面量或包含構造簽名的對象類型字面量來編寫。
那么什么是構造函數類型字面量呢?構造函數類型字面量是包含單個構造函數簽名的對象類型的簡寫。具體來說,構造函數類型字面量的形式如下:
new?<?T1,?T2,?...?>?(?p1,?p2,?...?)?=>?R
該形式與以下對象類型字面量是等價的:
{?new?<?T1,?T2,?...?>?(?p1,?p2,?...?)?:?R?}
下面我們來舉個實際的示例:
//?構造函數類型字面量new?(x:?number,?y:?number)?=>?Point
等價于以下對象類型字面量:
{?new?(x:?number,?y:?number):?Point;}
8.3 構造函數類型的應用
在介紹構造函數類型的應用前,我們先來看個例子:
interface?Point?{?new?(x:?number,?y:?number):?Point;??x:?number;??y:?number;}class?Point2D?implements?Point?{??readonly?x:?number;??readonly?y:?number;?constructor(x:?number,?y:?number)?{?this.x?=?x;?this.y?=?y;??}}const?point:?Point?=?new?Point2D(1,?2);
對于以上的代碼,TypeScript 編譯器會提示以下錯誤信息:
Class?'Point2D'?incorrectly?implements?interface?'Point'.Type?'Point2D'?provides?no?match?for?the?signature?'new?(x:?number,?y:?number):?Point'.
相信很多剛接觸 TypeScript 不久的小伙伴都會遇到上述的問題。要解決這個問題,我們就需要把對前面定義的 Point 接口進行分離,即把接口的屬性和構造函數類型進行分離:
interface?Point?{?x:?number;??y:?number;}interface?PointConstructor?{?new?(x:?number,?y:?number):?Point;}
完成接口拆分之后,除了前面已經定義的 Point2D 類之外,我們又定義了一個 newPoint 工廠函數,該函數用于根據傳入的 PointConstructor 類型的構造函數,來創建對應的 Point 對象。
class?Point2D?implements?Point?{??readonly?x:?number;??readonly?y:?number;?constructor(x:?number,?y:?number)?{?this.x?=?x;?this.y?=?y;??}}function?newPoint(??pointConstructor:?PointConstructor,??x:?number,??y:?number):?Point?{?return?new?pointConstructor(x,?y);}const?point:?Point?=?newPoint(Point2D,?1,?2);
8.4 使用泛型創建對象
了解完構造簽名和構造函數類型之后,下面我們來開始解決上面遇到的問題,首先我們需要重構一下 create 方法,具體如下所示:
class?GenericCreator<T>?{??create<T>(c:?{?new?():?T?}):?T?{?return?new?c();??}}
在以上代碼中,我們重新定義了 create 成員方法,根據該方法的簽名,我們可以知道該方法接收一個參數,其類型是構造函數類型,且該構造函數不包含任何參數,調用該構造函數后,會返回類型 T 的實例。
如果構造函數含有參數的話,比如包含一個 number 類型的參數時,我們可以這樣定義 create 方法:
create<T>(c:?{?new(a:?number):?T;?},?num:?number):?T?{?return?new?c(num);}
更新完 GenericCreator 泛型類,我們就可以使用下面的方式來創建 FirstClass 和 SecondClass 類的實例:
const?creator1?=?new?GenericCreator<FirstClass>();const?firstClass:?FirstClass?=?creator1.create(FirstClass);const?creator2?=?new?GenericCreator<SecondClass>();const?secondClass:?SecondClass?=?creator2.create(SecondClass);
好了,今天的分享就到這里,想要學習更多前端,可以QQ掃碼加入前端學習交流群,還有任何前端技術問題,可以在群內學習交流。
碼字不易,點個贊再走哦!趕緊分享給身邊的朋友吧!