前端開發系列045-基礎篇之TypeScript語言特性(五)
title: '前端開發系列045-基礎篇之TypeScript語言特性(五)'
tags:
- javaScript系列
- TypeScript系列
categories: []
date: 2017-11-24 18:05:13
本文主要對TypeScript中的泛型進行展開介紹。主要包括以下內容
❏ 泛型函式型別
❏ 泛型介面(Interface)
❏ 泛型類(Class)
❏ 泛型約束
一、泛型函式的型別
在以前的文章中,我們已經介紹了什麼是泛型函式,它跟普通函式還是有些區別的(泛型函式使用型別變數來佔位,具體型別值由函式呼叫傳參決定)。以前文章中介紹過TypeScript中的資料型別,以及可選的型別宣告。雖然並沒有必要(因為可以通過型別推導機制推匯出來),但我們確實能夠抽取出普通函式的具體型別。下面程式碼中demo函式的函式型別為:(name:string,age:number) => string
//檔案路徑 ../08-泛型函式/03-函式的型別.ts //[001] 函式的型別 //(1) 宣告demo函式 function demo(name:string,age:number):string { return "姓名:" +name + "年齡:" + age; } //(2) 把demo函式賦值給f let f:(name:string,age:number)=>string = demo; //使用demo函式的呼叫簽名 //let f:{(name:string,age:number):string} = demo; console.log(f("zs",18)); //姓名:zs年齡:18
接下來,我們花點時間研究,泛型函式的函式型別。其實泛型函式的型別與非泛型函式的型別本質上並沒由什麼不同,只是在最前面增加一個型別變數引數而已。下面給出具體的程式碼示例。
泛型函式的型別宣告可以使用不同的泛型引數,只要數量和使用方式一致即可。function demoT<T>(arg:T):T{ return arg; } //泛型函式demoT的型別為:<T>(arg:T) =>T let f1 : <T>(arg:T) =>T = demoT; //使用帶有呼叫簽名的物件字面量來定義泛型函式 let f2 : {<T>(arg:T) :T} = demoT; //可以使用不同的泛型引數名(這裡為X) let f3 : <X>(arg:X) =>X = demoT; //不使用型別宣告 let f4 = demoT; console.log(f1("abc")); //abc console.log(f2("哈哈")); //哈哈 console.log(f3("嘿嘿")); //嘿嘿 console.log(f4("咕嚕")); //咕嚕
二、泛型介面(Interface)
介面(Interface)指在面向物件程式語言中,不包含資料和邏輯但使用函式簽名定義行為的抽象型別。
TypeScript提供了介面特性,TypeScript的介面可以定義資料和行為,也可以擴充套件其它介面或者類。
在傳統面向物件程式設計範疇中,一個類可以被擴充套件為另外一個類,也可以實現一個或多個介面。實現某個介面可以被看做是簽署了一份協議,介面相當於協議,當我們簽署協議(實現介面)後,就必須遵守它的規則。
介面本身是抽象型別,其內容(規則)就是屬性和方法的簽名。
在前文中我們定義了泛型函式demoT,可以把demoT函式的簽名抽取並定義介面GenericFn,下面給出示例程式碼。
//檔案路徑 ../08-泛型函式/04-泛型介面.ts
//(1) 宣告泛型函式demoT
function demoT<T>(arg:T):T{
return arg;
}
//(2) 定義GenericFn介面
interface GenericFn{
<T>(arg: T): T;
}
let fn: GenericFn = demoT;
console.log(fn("哈哈")); //哈哈
有時候,我們可能需要把泛型引數(T)抽取成為整個介面的引數,好處是抽取後我們能夠清楚的知道使用的具體泛型型別是什麼,且介面中的其它成員也能使用。當我們使用泛型介面的時候,傳入一個型別引數來指定泛型型別即可,下面給出調整後的示例程式碼。
//檔案路徑 ../08-泛型函式/05-泛型介面02.ts
//(1) 宣告泛型函式demoT
function demoT<T>(arg:T):T{
return arg;
}
//(2) 定義泛型介面
interface GenericFn<T>{
(arg: T): T;
}
let f1: GenericFn<number> = demoT;
console.log(f1(123)); //123
//報錯:Argument of type '"字串"' is not assignable to parameter of type 'number'.
//console.log(f1("字串")); //錯誤的演示
let f2: GenericFn<string> = demoT;
console.log(f2("字串")); //字串
三、泛型類(Class)
泛型特性可以應用在Class身上,具體的使用方式和介面差不多。
//檔案路徑 ../08-泛型函式/06-泛型類.ts
//泛型類(Class)
class Person<T>{
//[1] 屬性部分
name:T;
color:T;
//[2] 方法部分
add:(a:T,b:T)=>T;
}
//獲取例項物件p1
var p1 = new Person<string>();
p1.name = "張三";
//報錯: TS2322: Type '123' is not assignable to type 'string'.
//p1.name = 123; 錯誤的演示
p1.color = "Red";
p1.add = function(a,b){
return a + b;
}
console.log(p1); //{name:"張三",color:"Red",...}
console.log(p1.add("ABC","-DEF")); //ABC-DEF
//獲取例項物件p2
var p2 = new Person<number>();
p2.name = 0;
p2.color = 1;
p2.add = function(a,b){
return a + b;
}
console.log(p2.add(100,200)); //300
上面的程式碼提供了泛型類使用的簡單示例,在定義泛型類的時候,只需要直接把泛型型別放在類名(這裡為Person)後面即可
,通過new呼叫類例項化的時候,以<型別>的方式傳遞,在Class中應用泛型可以幫助我們確認類中的很多屬性都在使用相同的型別,且能夠優化程式碼結構。
四、泛型約束
有時候,我們可能需要對泛型進行約束。下面的程式碼中我們聲明瞭泛型函式fn,並在fn的函式體中執行console.log("列印length值 = " + arg.length);
意在列印引數的長度。這份程式碼在編譯的時候會報錯,因為無法確定函式呼叫時傳入的引數一定擁有length屬性。
//檔案路徑 ../08-泛型函式/02-泛型函式使用注意點.ts
//說明 該泛型函式使用型別變數T來表示接收引數和返回值的型別
function fn<T>(arg:T):T{
console.log("列印length值 = " + arg.length);
return arg;
}
//報錯:error TS2339: Property 'length' does not exist on type 'T'.
console.log(fn([1,2,3]));
其實相比於操作any所有型別的資料而言,在這裡我們需要對引數型別進行限制,要求傳入的引數能夠擁有length屬性,這種場景可以使用泛型約束。
理想中泛型函式fn的工作情況是:“只要傳入的引數型別擁有指定的屬性length,那麼程式碼就應該正常執行。 為此,需要列出對於T的約束要求。下面,我們先定義一個介面來描述特定的約束條件。然後使用這個介面和extends關鍵字
來實現泛型約束,程式碼如下:
//檔案路徑 ../08-泛型函式/07-泛型約束.ts
//[001] 定義用於描述約束條件的介面
interface hasLengthP
{
length: number;
}
//[002] 宣告fn函式(應用了泛型約束)
function fn<T extends hasLengthP>(arg:T):T
{
console.log("列印length值 = " + arg.length);
return arg
}
//[003] 呼叫測試
console.log(fn([1,2,3])); //列印length值 = 3 [1,2,3];
console.log(fn({name:"zs",length:1})); //列印length值 = 1 物件內容
//說明:字串會被轉換為物件型別(基本包裝型別)
console.log(fn("測試")); //列印length值 = 2 測試
//報錯:error TS2345: Argument of type '123' is not assignable to parameter of type 'hasLengthP'.
console.log(fn(123)); //錯誤的演示
上面程式碼中的fn泛型函式被定義了約束,因此不再是適用於任意型別的引數。
我們需要傳入符合約束型別的值,傳入的實參必須擁有length屬性才能執行。
泛型約束中使用多重型別
提示 當宣告泛型約束的時候,我們只能夠關聯一種型別。但有時候,我們確實需要在泛型約束中使用多重型別,接下來我們研究下它的可能性和實現方式。
假設現在有一個泛型型別需要被約束,它只允許使用實現Interface_One和Interface_Two兩個介面的型別,考慮應該如何實現?
//檔案路徑 ../08-泛型函式/08-泛型約束中使用多重型別01.ts
//定義介面:Interface_One和Interface_Two
interface Interface_One{
func_One();
}
interface Interface_Two{
func_Two();
}
//泛型類(泛型約束為Interface_One,Interface_Two)
class classTest<T extends Interface_One,Interface_Two>
{
propertyDemo:T;
propertyDemoFunc(){
this.propertyDemo.func_One();
this.propertyDemo.func_Two();
}
}
我們可能會像這樣來定義泛型約束,然而上面的程式碼在編譯的時候會丟擲錯誤,也就是說我們不能在定義泛型約束的時候指定多個型別(上面的程式碼中我們指定了Interface_One和Interface_Two兩個型別),如果確實需要設計多重型別約束的泛型,可以通過把多重型別的介面轉換為一個超介面來處理,下面給出示例程式碼。
//檔案路徑 ../08-泛型函式/09-泛型約束中使用多重型別02.ts
//定義介面:Interface_One和Interface_Two
interface Interface_One{
func_One();
}
interface Interface_Two{
func_Two();
}
//Interface_One和Interface_Two成為了超介面,它們是Interface_T的父介面
interface Interface_T extends Interface_One,Interface_Two{};
//泛型類
class classTest<T extends Interface_T>
{
propertyDemo:T;
propertyDemoFunc(){
this.propertyDemo.func_One();
this.propertyDemo.func_Two();
}
}
let obj = {
func_One:function(){
console.log("func_One");
},
func_Two:function(){
console.log("func_Two");
}
}
//獲取例項化物件classTestA
let classTestA = new classTest();
classTestA.propertyDemo = obj;
classTestA.propertyDemoFunc(); //func_One func_Two
//下面是錯誤的演示
let classTestB = new classTest();
//報錯: Type '{ func_Two: () => void; }' is not assignable to type 'Interface_T'.
classTestA.propertyDemo = {
func_Two:function(){
console.log("func_Two_XXXX");
}
};
備註:該文章所有的示例程式碼均可以點選在Github託管倉庫獲取