如何通俗的解釋TypeScript 泛型

概述

在 TypeScript 中我們會使用泛型來對函數的相關類型進行約束。這裡的函數,同時包含 class 的構造函數,因此,一個類的聲明部分,也可以使用泛型。那麼,究竟什麼是泛型?如果通俗的理解泛型呢?

什麼是泛型

泛型(Generics)是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。

通俗的解釋,泛型是類型系統中的“參數”,主要作用是為瞭類型的重用。從上面定義可以看出,它隻會用在函數、接口和類中。它和js程序中的函數參數是兩個層面的事物(雖然意義是相同的),因為 typescript 是靜態類型系統,是在js進行編譯時進行類型檢查的系統,因此,泛型這種參數,實際上是在編譯過程中的運行時使用。之所以稱它為“參數”,是因為它具備和函數參數一模一樣的特性。

function increse(param) {
  // ...
}

而類型系統中,我們如此使用泛型:

function increase<T>(param: T): T {
  //...
}

當 param 為一個類型時,T 被賦值為這個類型,在返回值中,T 即為該類型從而進行類型檢查。

編譯系統

要知道 typescript 本身的類型系統也需要編程,隻不過它的編程方式很奇怪,你需要在它的程序代碼中穿插 js代碼(在 ts 代碼中穿插 js 代碼這個說法很怪,因為我們直觀的感覺是在 js 代碼中夾雜瞭 ts 代碼)。

編程中,最重要的一種形式就是函數。在 typescript 的類型編程中,你看到函數瞭嗎?沒有。這是因為,有泛型的地方就有函數,隻是函數的形式被 js 代碼給割裂瞭。typescript 需要進行編譯後得到最終產物。編譯過程中要做兩件事,一是在內存中運行類型編程的代碼,從而形成類型檢查體系,也就是說,我們能夠對 js 代碼進行類型檢查,首先是 typescript 編譯器運行 ts 編程代碼後得到瞭一個運行時的檢查系統本文來自否子戈的播客,運行這個系統,從而對穿插在其中的 js 代碼進行類型斷言;二是輸出 js,輸出過程中,編譯系統已經運行完瞭類型編程的代碼,就像php代碼中 echo js 代碼一樣,php代碼已經運行瞭,顯示出來的是 js 代碼。

從這個角度看 typescript,你或許更能理解為什麼說它是JavaScript的超集,為什麼它的編譯結果是 js。

通俗的理解泛型

既然我們理解瞭 ts 編譯系統的邏輯,那麼我們就可以把類型的編程和 js 本身的業務編程在情感上區分開。我們所講的“泛型”,隻存在於類型編程的部分,這部分代碼是 ts 的編譯運行時代碼。

我們來看下一個簡單的例子:

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,這樣我們就可以在接口中增加其他的方法。

可以註意到,即使 typescript 內置的基礎類型,例如 Array,被聲明為泛型接口、泛型類之後,這些接口和類在使用時必須通過<>傳入參數,本質上,因為它們都是函數,隻是返回值不同。

其他泛型使用的通俗解釋

接下來我們要再描述一個復雜的類型:

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 進行理解時的思路開拓,不適用於真實編程,特此聲明。

以上就是如何通俗的解釋TypeScript 泛型的詳細內容,更多關於TypeScript泛型的資料請關註WalkonNet其它相關文章!

推薦閱讀: