以爲 TypeScript 泛型有點難,想系統學習 TypeScript 泛型相關知識的小夥伴們看過來,本文從八個方面入手,全方位帶你一步步學習 TypeScript 中泛型,詳細的內容大綱請看下圖:javascript
軟件工程中,咱們不只要建立一致的定義良好的 API,同時也要考慮可重用性。 組件不只可以支持當前的數據類型,同時也能支持將來的數據類型,這在建立大型系統時爲你提供了十分靈活的功能。java
在像 C# 和 Java 這樣的語言中,可使用泛型來建立可重用的組件,一個組件能夠支持多種類型的數據。 這樣用戶就能夠以本身的數據類型來使用組件。node
設計泛型的關鍵目的是在成員之間提供有意義的約束,這些成員能夠是:類的實例成員、類的方法、函數參數和函數返回值。typescript
爲了便於你們更好地理解上述的內容,咱們來舉個例子,在這個例子中,咱們將一步步揭示泛型的做用。首先咱們來定義一個通用的 identity
函數,該函數接收一個參數並直接返回它:shell
function identity (value) { return value; } console.log(identity(1)) // 1
如今,咱們將 identity
函數作適當的調整,以支持 TypeScript 的 Number 類型的參數:segmentfault
function identity (value: Number) : Number { return value; } console.log(identity(1)) // 1
這裏 identity
的問題是咱們將 Number
類型分配給參數和返回類型,使該函數僅可用於該原始類型。但該函數並非可擴展或通用的,很明顯這並非咱們所但願的。數組
咱們確實能夠把 Number
換成 any
,咱們失去了定義應該返回哪一種類型的能力,而且在這個過程當中使編譯器失去了類型保護的做用。咱們的目標是讓 identity
函數能夠適用於任何特定的類型,爲了實現這個目標,咱們可使用泛型來解決這個問題,具體實現方式以下:sass
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 類型。async
其中 T
表明 Type,在定義泛型時一般用做第一個類型變量名稱。但實際上 T
能夠用任何有效名稱代替。除了 T
以外,如下是常見泛型變量表明的意思:
其實並非只能定義一個類型變量,咱們能夠引入但願定義的任何數量的類型變量。好比咱們引入一個新的類型變量 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]),感謝 Assassin丶 指出。
(圖片來源:https://medium.com/better-pro...)
如你所見,該函數接收你傳遞給它的任何類型,使得咱們能夠爲不一樣類型建立可重用的組件。如今咱們再來看一下 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: number Semlinker: 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()); // 68 const 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 泛型提供的其餘功能。
有時咱們可能但願限制每一個類型變量接受的類型數量,這就是泛型約束的做用。下面咱們來舉幾個例子,介紹一下如何使用泛型約束。
有時候,咱們但願類型變量對應的類型上存在某些屬性。這時,除非咱們顯式地將特定屬性定義爲類型變量,不然編譯器不會知道它們的存在。
一個很好的例子是在處理字符串或數組時,咱們會假設 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; } // or function identity<T>(arg: Array<T>): Array<T> { console.log(arg.length); return arg; }
泛型約束的另外一個常見的使用場景就是檢查對象上的鍵是否存在。不過在看具體示例以前,咱們得來了解一下 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'); // 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"'.(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 : never type 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>; // string type extractPersonPromise = UnPromisify<typeof personPromise>; // Person
爲了方便開發者 TypeScript 內置了一些經常使用的工具類型,好比 Partial、Required、Readonly、Record 和 ReturnType 等。出於篇幅考慮,這裏咱們只簡單介紹其中幾個經常使用的工具類型。
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; }
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" } };
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 };
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
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>; // 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
簡單介紹了泛型工具類型,最後咱們來介紹如何使用泛型來建立對象。
有時,泛型類可能須要基於傳入的泛型 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 < ... > ( ... )
介紹完構造簽名,咱們再來介紹一個與之相關的概念,即構造函數類型。
在 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; }
在介紹構造函數類型的應用前,咱們先來看個例子:
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);
瞭解完構造簽名和構造函數類型以後,下面咱們來開始解決上面遇到的問題,首先咱們須要重構一下 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);