你不知道的 TypeScript 泛型
一、泛型是什么
軟件工程中,我們不僅要創建一致的定義良好的 API,同時也要考慮可重用性。組件不僅能夠支持當前的數據類型,同時也能支持未來的數據類型,這在創建大型系統時為你提供了十分靈活的功能。
泛型表示泛指某一種類型,開發者可以指定一個表示類型的變量,用它來作為實際類型的占位符,用尖括號來包裹類型變量 。泛型的主要作用是創建可重用的組件,從而讓一個組件可以支持多種數據類型,它可以作用在接口、類、函數或類型別名上。
下面我們來舉個例子,幫助大家更好地理解上述的內容。在這個例子中,我們將一步步揭示泛型的作用。
1.1 identity 函數示例
首先我們來定義一個通用的 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 泛型的讀者來說,首次看到 語法會感到陌生。但這沒什么可擔心的,就像傳遞參數一樣,我們傳遞了我們想要用于特定函數調用的類型。
參考上面的圖片,當我們調用 identity(1) ,number 類型就像參數 1 一樣,它將在出現 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 函數:
- function identity <T, U>(value: T, message: U) : T {
- console.log(message);
- return value;
- }
相比之前定義的 identity 函數,新的 identity 函數增加了一個類型變量 U,但該函數的返回類型我們仍然使用 T。如果我們想要返回兩種類型的對象該怎么辦呢?針對這個問題,我們有多種方案,其中一種就是使用 TypeScript 中的元組:
- 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: number
- Semlinker: string
- {value: 68, message: "Semlinker"}
泛型除了可以應用在函數和接口之外,它也可以應用在類中,下面我們就來看一下在類中如何使用泛型。
三、泛型類
在類中使用泛型也很簡單,我們只需要在類名后面,使用 的語法定義任意多個類型變量,具體示例如下:
- 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()); // 68
- const myStringClass = new IdentityClass<string>("Semlinker!");
- console.log(myStringClass.getIdentity()); // Semlinker!
接下來我們以實例化 myNumberClass 為例,來分析一下其調用過程:
- 在實例化 IdentityClass 對象時,我們傳入 number 類型和構造函數參數值 68;
- 之后在 IdentityClass 類中,類型變量 T 的值變成 number 類型;
- IdentityClass 類實現了 GenericInterface,而此時 T 表示 number 類型,因此等價于該類實現了 GenericInterface 接口;
- 而對于 GenericInterface 接口來說,類型變量 U 也變成了 number。這里我有意使用不同的變量名,以表明類型值沿鏈向上傳播,且與變量名無關。
相信看到這里一些讀者會有疑問,我們什么時候需要使用泛型呢?通常在決定是否使用泛型時,我們有以下兩個參考標準:
- 當你的函數、接口或類將處理多種數據類型時;
- 當函數、接口或類在多個地方使用該數據類型時。
很有可能你沒有辦法保證在項目早期就使用泛型的組件,但是隨著項目的發展,組件的功能通常會被擴展。這種增加的可擴展性最終很可能會滿足上述兩個條件,在這種情況下,引入泛型將比復制組件來滿足一系列數據類型更方便。
我們將在本文的后面探討更多滿足這兩個條件的用例。不過在這樣做之前,讓我們先介紹一下 Typescript 泛型提供的其他功能。
四、泛型約束
有時我們可能希望限制每個類型變量接受的類型數量,這就是泛型約束的作用。下面我們來舉幾個例子,介紹一下如何使用泛型約束。
4.1 確保屬性存在
有時候,我們希望類型變量對應的類型上存在某些屬性。這時,除非我們顯式地將特定屬性定義為類型變量,否則編譯器不會知道它們的存在。
一個很好的例子是在處理字符串或數組時,我們會假設 length 屬性是可用的。讓我們再次使用 identity 函數并嘗試輸出參數的長度:
- function identity<T>(arg: T): T {
- // Property 'length' does not exist on type 'T'.(2339)
- 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)
此外,我們還可以使用 , 號來分隔多種約束類型,比如:。而對于上述的 length 屬性問題來說,如果我們顯式地將變量設置為數組類型,也可以解決該問題,具體方式如下:
- function identity<T>(arg: T[]): 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
提示:TypeScript Playground v4.2.3 版本以上的編譯器不會顯示 keyof 操作符的結果
通過 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'); // OK
- let 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"'.
很明顯通過使用泛型約束,在編譯階段我們就可以提前發現錯誤,大大提高了程序的健壯性和穩定性。接下來,我們來介紹一下泛型參數默認類型。
五、泛型參數默認類型
在 TypeScript 2.3 以后,我們可以為泛型中的類型參數指定默認類型。當使用泛型時沒有在代碼中直接指定類型參數,從實際值參數中也無法推斷出類型時,這個默認類型就會起作用。
泛型參數默認類型與普通函數默認值類似,對應的語法很簡單,即 ,對應的使用示例如下:
- interface Person<T=string> {
- id: T;
- }
- const p0: Person = { id: "lolo" };
- const p1: Person<number> = { id: 28 };
泛型參數的默認類型遵循以下規則:
- 有默認類型的類型參數被認為是可選的。
- 必選的類型參數不能在可選的類型參數后。
- 如果類型參數有約束,類型參數的默認類型必須滿足這個約束。
- 當指定類型實參時,你只需要指定必選類型參數的類型實參。未指定的類型參數會被解析為它們的默認類型。
- 如果指定了默認類型,且類型推斷無法選擇一個候選類型,那么將使用默認類型作為推斷結果。
- 一個被現有類或接口合并的類或者接口的聲明可以為現有類型參數引入默認類型。
- 一個被現有類或接口合并的類或者接口的聲明可以引入新的類型參數,只要它指定了默認類型。
六、泛型條件類型
在 TypeScript 2.8 中引入了條件類型,使得我們可以根據某些條件得到不同的類型,這里所說的條件是類型兼容性約束。條件類型會以一個條件表達式進行類型關系檢測,從而在兩種類型中選擇其一:
- 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 : never
- type StrDictMember = DictMember<StrDict> // string
在上面示例中,當類型 T 滿足 T extends Dictionary 約束時,我們會使用 infer 關鍵字聲明了一個類型變量 V,并返回該類型,否則返回 never 類型。
在 TypeScript 中,never 類型表示的是那些永不存在的值的類型。例如, never 類型是那些總是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型。
另外,需要注意的是,沒有類型是 never 的子類型或可以賦值給 never 類型(除了 never 本身之外)。即使 any 也不可以賦值給 never。
條件類型還有一個特性:分布式條件類型。當檢測的類型是由 ”裸類型“(指該類型未被包裝過) 組成的聯合類型時,條件類型會被自動分發成聯合類型。以 T extends U ? X : Y 條件類型為例,當類型參數的為 A | B | C 時,該條件類型將會被解析為 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。
分布式條件類型的使用示例如下:
- type TypeName<T> = T extends string
- ? "string"
- : T extends number
- ? "number"
- : T extends boolean
- ? "boolean"
- : T extends undefined
- ? "undefined"
- : T extends Function
- ? "function"
- : "object";
- type T10 = TypeName<string | (() => void)>; // "string" | "function"
- type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
- type T11 = TypeName<string[] | number[]>; // "object"
七、泛型工具類型
為了方便開發者 TypeScript 內置了一些常用的工具類型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考慮,這里我們只簡單介紹其中幾個常用的工具類型。
7.1 Partial
Partial 的作用就是將某個類型里的屬性全部變為可選項 ?。
定義:
- type Partial<T> = {
- [P in keyof T]?: T[P];
- };
以上 Partial 類型被稱為映射類型,用于把已有的類型轉換成新的類型。在以上代碼中,我們首先通過 keyof T 拿到 T 的所有屬性名,然后使用 in 進行遍歷,將值賦給類型變量 P,最后通過 T[P] 取得屬性 P 對應的類型。中間的 ? 號,表示將屬性變為可選。
示例:
- interface Todo {
- title: string;
- description: string;
- }
- function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
- return { ...todo, ...fieldsToUpdate };
- }
- const todo1 = {
- title: "Learn TS",
- description: "Learn TypeScript"
- };
- const todo2 = updateTodo(todo1, {
- description: "Learn TypeScript Handbook"
- });
在上面的 updateTodo 方法中,我們利用 Partial 工具類型,定義 fieldsToUpdate 的類型為 Partial,即:
- {
- title?: string | undefined;
- description?: string | undefined;
- }
是不是覺得 Partial 使用起來挺簡單的,那么如何定義一個 SetOptional 工具類型,支持把給定的 keys 對應的屬性變成可選的。對應的使用示例如下所示:
- type Foo = {
- a: number;
- b?: string;
- c: boolean;
- }
- // 測試用例
- type SomeOptional = SetOptional<Foo, 'a' | 'b'>;
- // type SomeOptional = {
- // a?: number; // 該屬性已變成可選的
- // b?: string; // 保持不變
- // c: boolean;
- // }
7.2 Record
Record 的作用是將 K 中所有的屬性的值轉化為 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 的作用是將某個類型中的子屬性挑出來,變成包含這個類型部分屬性的子類型。
定義:
- 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: "Learn TS",
- completed: false
- };
在掌握 Pick 的用法之后,你可以想一下,如何定義一個 ConditionalPick 工具類型,支持根據指定的 Condition 條件來生成新的類型,對應的使用示例如下:
- interface Example {
- a: string;
- b: string | number;
- c: () => void;
- d: {};
- }
- // 測試用例:
- type StringKeysOnly = ConditionalPick<Example, string>;
- //=> { a: string }
7.4 Exclude
Exclude 的作用是將某個類型中屬于另一個的類型移除掉。
定義:
- 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
由以上結果可知,Exclude 工具類型利用了前面介紹的分布式條件類型的特性。
7.5 ReturnType
ReturnType 的作用是用于獲取函數 T 的返回類型。
定義:
- type ReturnType any> = T extends (...args: any) => infer R ? R : any;
示例:
- type T0 = ReturnType<() => string>; // string
- type T1 = ReturnType<(s: string) => void>; // void
- type T2 = ReturnType<<T>() => T>; // {}
- type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
- type T4 = ReturnType<any>; // any
- type T5 = ReturnType<never>; // any
- type T6 = ReturnType<string>; // Error
- type 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。在通用的 GenericCreator 泛型類中,我們定義了一個名為 create 的成員方法,該方法會使用 new 關鍵字來調用傳入的實際類型的構造函數,來創建對應的對象。但可惜的是,以上代碼并不能正常運行,對于以上代碼,在 TypeScript v4.4.3 編譯器下會提示以下錯誤:
- '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);
8.5 抽象構造簽名
在 TypeScript 4.2 版本中引入了抽象構造簽名,用于解決以下的問題:
- type ConstructorFunction = new (...args: any[]) => any;
- abstract class Utilities {}
- // Type 'typeof Utilities' is not assignable to type 'ConstructorFunction'.
- // Cannot assign an abstract constructor type to a non-abstract constructor type.
- let UtilityClass: ConstructorFunction = Utilities; // Error.
由以上的錯誤信息可知,我們不能把抽象構造器類型分配給非抽象的構造器類型。針對這個問題,我們需要使用 abstract 修飾符:
- declare type ConstructorFunction = abstract new (...args: any[]) => any;
需要注意的是,對于抽象構造器類型,我們也可以傳入具體的實現類:
- declare type ConstructorFunction = abstract new (...args: any[]) => any;
- abstract class Utilities {}
- class UtilitiesConcrete extends Utilities {}
- let UtilityClass: ConstructorFunction = Utilities; // Ok
- let UtilityClass1: ConstructorFunction = UtilitiesConcrete; // Ok
而對于 TypeScript 4.2 以下的版本,我們可以通過以下方式來解決上面的問題:
- type Constructor<T> = Function & { prototype: T }
- abstract class Utilities {}
- class UtilitiesConcrete extends Utilities {}
- let UtilityClass: Constructor<Utilities> = Utilities;
- let UtilityClass1: Constructor<UtilitiesConcrete> = UtilitiesConcrete;
九、可變元組類型
在 TypeScript 4.0 版本支持可變元組類型,其中有兩個新的變化。第一個變化是元組類型的展開運算可以支持泛型了:
- function tail<T extends any[]>(arr: readonly [any, ...T]) {
- const [_ignored, ...rest] = arr;
- return rest;
- }
- const myTuple = [1, 2, 3, 4] as const;
- const myArray = ["hello", "world"];
- const r1 = tail(myTuple); // r1: [2, 3, 4]
- const r2 = tail([...myTuple, ...myArray] as const); // r2: [2, 3, 4, ...string[]]
第二個變化是 rest 元素可以出現在元組中的任何位置,而不僅僅是在結尾!
- type Strings = [string, string];
- type Numbers = [number, number];
- type StrStrNumNumBool = [...Strings, ...Numbers, boolean];
對于 TypeScript 4.0 以下的版本,以上代碼將會出現以下的錯誤信息:
- A rest element must be last in a tuple type.(1256)
利用這兩個特性,我們就可以實現一個類型良好的 concat 函數:
- type Arr = readonly any[];
- function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
- return [...arr1, ...arr2];
- }
- const arr3 = concat([1, 2, 3], ["a", "b", "c"])
關于可變元組類型的相關內容,就不展開介紹了,感興趣的小伙伴可以自行閱讀 TypeScript 4.0 的相關文檔。
十、泛型是如何工作的
最后,阿寶哥將使用 ts-ast-viewer 在線工具,帶大家換個角度來學習 TypeScript 的泛型。對應的示例代碼如下:
- type Head<T extends Array<any>> = T extends [any, ...any] ? T[0] : never
- type H0 = Head<[1, 2, 3]> // 1
10.1 類型變量 AST
10.2 條件類型 AST
10.3 類型引用 AST
所使用在線工具的地址為:https://ts-ast-viewer.com/
建議大家實際使用一下 ts-ast-viewer 這個在線工具,詳細看一下生成的節點,這樣的話,可以讓你更好地理解 TypeScript 的泛型。