軟體工程中很重要的一部分是建立元件,這些元件不僅具有定義良好且一致的 API,而且還能重複使用。能夠處理當前資料以及未來資料的元件,將為您提供建置大型軟體系統最靈活的功能。
在 C# 和 Java 等語言中,用於建立可重複使用元件的工具箱中的一個主要工具是泛型,也就是說,能夠建立一個元件,可以在各種型別上執行作業,而不是單一型別。這允許使用者使用這些元件並使用自己的型別。
泛型的 Hello World
首先,我們來做泛型的「hello world」:身分函數。身分函數是一個函數,它會傳回傳入的任何值。您可以將它想成類似於 echo
命令。
如果不使用泛型,我們必須為身分函數指定一個特定型別
tsTry
functionidentity (arg : number): number {returnarg ;}
或者,我們可以使用 any
型別來描述身分函數
tsTry
functionidentity (arg : any): any {returnarg ;}
雖然使用 any
肯定在於它會讓函數接受 arg
型別的任何和所有型別,但實際上我們會在函數傳回時失去該型別的資訊。如果我們傳入一個數字,我們唯一擁有的資訊就是可以傳回任何型別。
相反地,我們需要一種方法來擷取引數的型別,並以一種方式使用它,以便我們也可以用它來表示傳回的內容。在此,我們將使用型別變數,一種特殊類型的變數,它作用於型別而不是值。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}
我們現在已將型別變數 T
新增到身分函數。這個 T
允許我們擷取使用者提供的型別(例如 number
),以便我們稍後可以使用該資訊。在此,我們再次使用 T
作為傳回型別。經過檢查,我們現在可以看到引數和傳回型別使用相同的型別。這允許我們在函數的一端傳輸該型別資訊,並從另一端輸出。
我們說這個版本的 identity
函數是泛型的,因為它可以在各種型別上執行作業。與使用 any
不同,它也和使用數字作為引數和傳回型別的第一個 identity
函數一樣精確(也就是說,它不會遺失任何資訊)。
一旦我們寫好泛型身分函數,我們就可以用兩種方式之一來呼叫它。第一種方法是將所有引數,包括型別引數,傳遞給函數
tsTry
letoutput =identity <string>("myString");
在此我們明確地設定 T
為 字串
作為函式呼叫的其中一個引數,使用 <>
表示引數,而不是 ()
。
第二種方式或許也是最常見的方式。在此我們使用型別引數推論,也就是說,我們希望編譯器根據我們傳入的引數型別自動設定 T
的值
tsTry
letoutput =identity ("myString");
請注意,我們不必在尖括號 (<>
) 中明確傳遞型別;編譯器只會檢視值 "myString"
,然後將 T
設定為其型別。雖然型別引數推論可以幫助縮短程式碼並使其更易於閱讀,但當編譯器無法推論型別時,您可能需要明確傳入型別引數,就像我們在前面的範例中所做的那樣,這可能會發生在更複雜的範例中。
使用泛型型別變數
當您開始使用泛型時,您會注意到,當您建立泛型函式(例如 identity
)時,編譯器會強制您在函式主體中正確使用任何泛型型別參數。也就是說,您實際上將這些參數視為任何和所有型別。
讓我們來看一下我們前面提到的 identity
函式
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}
如果我們還想將引數 arg
的長度記錄到主控台,每次呼叫時都會記錄,我們可能會想這樣寫
tsTry
functionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
當我們這樣做時,編譯器會給我們一個錯誤,指出我們正在使用 arg
的 .length
成員,但我們沒有在任何地方說過 arg
有這個成員。請記住,我們前面說過,這些型別變數代表任何和所有型別,因此使用這個函式的人可能會傳入一個 數字
,而 數字
沒有 .length
成員。
假設我們實際上打算讓這個函式作用於 T
的陣列,而不是直接作用於 T
。由於我們使用的是陣列,因此 .length
成員應該是可用的。我們可以像建立其他型別的陣列一樣來描述這個陣列
tsTry
functionloggingIdentity <T >(arg :T []):T [] {console .log (arg .length );returnarg ;}
您可以將 loggingIdentity
的型別讀成「泛型函式 loggingIdentity
有一個型別參數 T
,以及一個引數 arg
,它是一個 T
的陣列,並傳回一個 T
的陣列。」如果我們傳入一個數字陣列,我們會得到一個數字陣列,因為 T
會繫結到 數字
。這讓我們可以使用泛型型別變數 T
作為我們正在使用的型別的一部分,而不是整個型別,這讓我們有更大的靈活性。
我們也可以用這種方式撰寫範例
tsTry
functionloggingIdentity <T >(arg :Array <T >):Array <T > {console .log (arg .length ); // Array has a .length, so no more errorreturnarg ;}
您可能已經熟悉其他語言中的這種類型樣式。在下一節中,我們將介紹如何建立您自己的泛型類型,例如 Array<T>
。
泛型類型
在先前的各節中,我們建立了泛型的識別函數,它們適用於各種類型。在本節中,我們將探討函數本身的類型,以及如何建立泛型介面。
泛型函數的類型就像非泛型函數的類型,首先列出類型參數,類似於函數宣告
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <T >(arg :T ) =>T =identity ;
我們也可以在類型中使用不同的名稱作為泛型類型參數,只要類型變數的數量和類型變數的使用方式一致即可。
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : <U >(arg :U ) =>U =identity ;
我們也可以將泛型類型寫成物件文字類型的呼叫簽章
tsTry
functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity : { <T >(arg :T ):T } =identity ;
這讓我們可以寫出我們的第一個泛型介面。讓我們從前一個範例中取出物件文字,並將它移到介面中
tsTry
interfaceGenericIdentityFn {<T >(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn =identity ;
在類似的範例中,我們可能想要將泛型參數移到整個介面的參數。這讓我們可以看到我們泛型化的類型(例如 Dictionary<string>
,而不是只有 Dictionary
)。這會讓類型參數對介面的所有其他成員可見。
tsTry
interfaceGenericIdentityFn <T > {(arg :T ):T ;}functionidentity <T >(arg :T ):T {returnarg ;}letmyIdentity :GenericIdentityFn <number> =identity ;
請注意,我們的範例已變更為稍有不同的內容。我們現在不再描述泛型函數,而是一個非泛型函數簽章,它是泛型類型的一部分。當我們使用 GenericIdentityFn
時,我們現在也需要指定對應的類型引數(在此處為 number
),有效地鎖定底層呼叫簽章將使用的內容。了解何時將類型參數直接放在呼叫簽章上,以及何時將其放在介面上本身,將有助於描述類型的哪些方面是泛型的。
除了泛型介面之外,我們也可以建立泛型類別。請注意,無法建立泛型列舉和命名空間。
泛型類別
泛型類別的形狀類似於泛型介面。泛型類別在類別名稱後面有尖括號 (<>
) 中的泛型類型參數清單。
tsTry
classGenericNumber <T > {zeroValue :T ;add : (x :T ,y :T ) =>T ;}letmyGenericNumber = newGenericNumber <number>();myGenericNumber .zeroValue = 0;myGenericNumber .add = function (x ,y ) {returnx +y ;};
這是使用 `GenericNumber` 類別相當直接的方式,但你可能已經注意到沒有任何限制只能使用 `number` 類型。我們也可以使用 `string` 甚至更複雜的物件。
tsTry
letstringNumeric = newGenericNumber <string>();stringNumeric .zeroValue = "";stringNumeric .add = function (x ,y ) {returnx +y ;};console .log (stringNumeric .add (stringNumeric .zeroValue , "test"));
就像介面一樣,將類型參數放在類別本身上,讓我們確保類別的所有屬性都使用相同的類型。
正如我們在 類別章節 中所述,類別的類型有兩個面向:靜態面向和實例面向。泛型類別僅在其實例面向而非靜態面向中是泛型的,因此在使用類別時,靜態成員無法使用類別的類型參數。
泛型約束
如果你還記得先前的範例,你可能有時會想要撰寫一個泛型函式,針對一組類型運作,而你對這組類型的功能有些了解。在我們的 `loggingIdentity` 範例中,我們想要存取 `arg` 的 `.length` 屬性,但編譯器無法證明每個類型都有 `.length` 屬性,因此它會警告我們無法做出這個假設。
tsTry
functionloggingIdentity <T >(arg :T ):T {Property 'length' does not exist on type 'T'.2339Property 'length' does not exist on type 'T'.console .log (arg .); length returnarg ;}
我們不想要使用任何和所有類型,我們希望將這個函式限制為與任何和所有也具有 `.length` 屬性的類型一起使用。只要類型有這個成員,我們就會允許它,但它至少需要有這個成員。為此,我們必須將我們的需求列為 T 可以是什麼的約束。
為此,我們將建立一個介面來描述我們的約束。在這裡,我們將建立一個具有單一 `.length` 屬性的介面,然後我們將使用這個介面和 `extends` 關鍵字來表示我們的約束
tsTry
interfaceLengthwise {length : number;}functionloggingIdentity <T extendsLengthwise >(arg :T ):T {console .log (arg .length ); // Now we know it has a .length property, so no more errorreturnarg ;}
由於泛型函數現在受到約束,因此它不再適用於任何和所有類型
tsTry
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.2345Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.loggingIdentity (3 );
相反,我們需要傳入類型具有所有必要屬性的值
tsTry
loggingIdentity ({length : 10,value : 3 });
在泛型約束中使用類型參數
您可以宣告受另一個類型參數約束的類型參數。例如,我們希望從物件取得屬性,並提供其名稱。我們希望確保不會意外擷取不存在於 obj
上的屬性,因此我們會在兩個類型之間放置約束
tsTry
functiongetProperty <T ,K extends keyofT >(obj :T ,key :K ) {returnobj [key ];}letx = {a : 1,b : 2,c : 3,d : 4 };getProperty (x , "a");Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.2345Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.getProperty (x ,"m" );
在泛型中使用類別類型
在 TypeScript 中使用泛型建立工廠時,必須透過其建構函數來參照類別類型。例如,
tsTry
functioncreate <T >(c : { new ():T }):T {return newc ();}
更進階的範例使用原型屬性來推論和約束建構函數與類別類型實例側之間的關係。
tsTry
classBeeKeeper {hasMask : boolean;}classZooKeeper {nametag : string;}classAnimal {numLegs : number;}classBee extendsAnimal {keeper :BeeKeeper ;}classLion extendsAnimal {keeper :ZooKeeper ;}functioncreateInstance <A extendsAnimal >(c : new () =>A ):A {return newc ();}createInstance (Lion ).keeper .nametag ;createInstance (Bee ).keeper .hasMask ;