在 TypeScript 中咱們會使用泛型來對函數的相關類型進行約束。這裏的函數,同時包含 class 的構造函數,所以,一個類的聲明部分,也可使用泛型。那麼,究竟什麼是泛型?若是通俗的理解泛型呢?php
泛型(Generics)是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。html
通俗的解釋,泛型是類型系統中的「參數」,主要做用是爲了類型的重用。從上面定義能夠看出,它只會用在函數、接口和類中。它和 js 程序中的函數參數是兩個層面的事物(雖然意義是相同的),由於 typescript 是靜態類型系統,是在 js 進行編譯時進行類型檢查的系統,所以,泛型這種參數,其實是在編譯過程當中的運行時使用。之因此稱它爲「參數」,是由於它具有和函數參數如出一轍的特性。git
function increse(param) {
// ...
}複製代碼
而類型系統中,咱們如此使用泛型:github
function increase<T>(param: T): T {
//...
}複製代碼
當 param 爲一個類型時,T 被賦值爲這個類型,在返回值中,T 即爲該類型從而進行類型檢查。typescript
要知道 typescript 自己的類型系統也須要編程,只不過它的編程方式很奇怪,你須要在它的程序代碼中穿插 js 代碼(在 ts 代碼中穿插 js 代碼這個說法很怪,由於咱們直觀的感受是在 js 代碼中夾雜了 ts 代碼)。編程
編程中,最重要的一種形式就是函數。在 typescript 的類型編程中,你看到函數了嗎?沒有。這是由於,有泛型的地方就有函數,只是函數的形式被 js 代碼給割裂了。typescript 須要進行編譯後獲得最終產物。編譯過程當中要作兩件事,一是在內存中運行類型編程的代碼,從而造成類型檢查體系,也就是說,咱們可以對 js 代碼進行類型檢查,首先是 typescript 編譯器運行 ts 編程代碼後獲得了一個運行時的檢查系統本文來自否子戈的播客,運行這個系統,從而對穿插在其中的 js 代碼進行類型斷言;二是輸出 js,輸出過程當中,編譯系統已經運行完了類型編程的代碼,就像 php 代碼中 echo js 代碼同樣,php 代碼已經運行了,顯示出來的是 js 代碼。bash
從這個角度看 typescript,你或許更能理解爲何說它是 JavaScript 的超集,爲何它的編譯結果是 js(爲何不能夠將 ts 編譯爲其餘語言呢?)。閉包
既然咱們理解了 ts 編譯系統的邏輯,那麼咱們就能夠把類型的編程和 js 自己的業務編程在情感上區分開。咱們所講的「泛型」,只存在於類型編程的部分,這部分代碼是 ts 的編譯運行時代碼。ide
咱們來看下一個簡單的例子:函數
function increase<T>(param: T): T {
//...
}複製代碼
這段代碼,若是咱們把 js 代碼區分開,而後用類型描述文原本表示會是怎樣?
// 聲明函數 @type,參數爲 T,返回結果爲 (T): T
@type = T => (T): T
// 運行函數獲得一個類型 F,即類型爲 (number): number
@F = @type(number)
// 要求 increase 這個函數符合 F 這種類型,也就是參數爲 number,返回值也爲 number
@@F
function increase(param) {
// ...
}
@@end複製代碼
實際上沒有 @@F 這種語法,是我編造出來的,目的是讓你能夠從另外一個角度去看類型系統。
當咱們理解泛型是一種「參數」以後,咱們可能會問:類型系統的函數在哪裏?對於 js 函數而言,你能夠很容易指出函數聲明語句和參數,可是 ts 中,這個部分是隱藏起來的。不過,咱們能夠在一些特定結構中,比較容易看到類型函數的影子:
// 聲明一個泛型接口,這個寫法,像極了聲明一個函數,咱們用描述語言來形容 @type = T => (T): T
interface GenericIdentityFn<T> {
(arg: T): T;
}
// 這個寫法,有點像一個閉包函數,在聲明函數後,當即運行這個函數,描述語言:@@[T => (T): T](any)
function identity<T>(arg: T): T {
return arg;
}
// 使用泛型接口,像極了調用一個函數,咱們用描述語言來形容 @type(number)
let myIdentity: GenericIdentityFn<number> = identity;複製代碼
上面這一整段代碼,咱們用描述文本重寫一遍:
@GenericIdentityFn = T => (T): T
@@[T => (T): T](any)
function identify(arg) {
return arg
}
@@end
@@GenericIdentityFn(number)
let myIdentity = identity
@@end複製代碼
咱們在類型系統中聲明瞭兩個函數,分別是 @GenericIdentityFn 和 @some(匿名函數 @[T => (T): T])。雖然是兩個函數,可是實際上,它們的是如出一轍的,由於 typescript 是結構類型,也就是在類型檢查的時候只判斷結構上的每一個節點類型是否相同,而不是必須保持類型變量自己的指針相同。@GenericIdentityFn 和 @some 這兩個函數分別被調用,用來修飾 identify 和 myIdentify,在調用的時候,接收的參數不一樣,因此致使最終的類型檢查規則是不一樣的,identify 只要保證參數和返回值的類型相同,至於具體什麼類型,any。而 myIdentify 除了保證參數返回值類型相同外,還要求類型必須是 number。
除了泛型接口,class 類也能夠泛型化,即「泛型類」,藉助泛型類,咱們來探究一下泛型的聲明和使用的步驟。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();複製代碼
前文泛型接口由於只是爲了約束函數的類型,因此寫的很像函數,實際上,咱們能夠用描述語言從新描述一個泛型接口和泛型類。上面的紅色部分,咱們用描述語言來描述:
@GenericNumber = T => class {
zeroValue: T;
add: (x: T, y: T) => T;
}複製代碼
@GenericNumber 這個函數,以 T 爲參數,返回一個 class,在 @type 函數體內屢次用到了參數 T。
@GenericIdentityFn = T => interface {
(arg: T): T;
}複製代碼
咱們從新描述了前面的 interface GenericIdentityFn,這樣咱們就能夠在接口中增長其餘的方法。
接下來咱們要再描述一個複雜的類型:
class Animal {
numLegs: number;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}複製代碼
咱們姑且不去看 new() 的部分,咱們看尖括號中的 extends 語法,這裏應該怎麼理解呢?實際上,咱們面對的問題是,在編譯時,<A extends Animal> 尖括號中的內容是何時運行的,是以前,仍是之間?
// 究竟是
@type = (A extends Animal) => (new() => A): A
@type(T)
// 仍是
@type = A => (new() => A): A
@type(T extends Animal)複製代碼
由於 typescript 是靜態類型系統,Animal 是不變的類,所以,能夠推測其實在類的建立以前,尖括號的內容已經被運行了。
@type = (A extends Animal) => (new() => A): A複製代碼
也就是說,要使用 @type(T) 產生類型,首先 T 要知足 Animal 的結構,而後才能獲得須要的類型,若是 T 已經不知足 Animal 類的結構了,那麼編譯器會直接報錯,而這個報錯,不是類型檢查階段,而是在類型系統的建立階段,也就是 ts 代碼的運行階段。這種狀況被稱爲「泛型約束」。
另外,相似 <A,B> 這樣的語法其實和函數參數一致。
@type = (A, B) => (A|B): SomeType複製代碼
咱們再來看 ts 內置的基礎類型:Array<number>
@Array = any => any[]複製代碼
Typescript 中的泛型,實際上就是類型的生成函數的參數。本文的內容所有爲憑空想象,僅適用於對 ts 進行理解時的思路開拓,不適用於真實編程,特此聲明。
本文發佈於個人博客:www.tangshuang.net/7473.html
歡迎來個人播客與我交流,也但願你經過下方二維碼給我打賞。