1. 程式人生 > 其它 >前端開發系列045-基礎篇之TypeScript語言特性(五)

前端開發系列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託管倉庫獲取