Typescript - 泛型

泛型,顧名思義廣泛的類型,是一種讓組件一次支持多種類型的技術(shù)方案。大型軟件系統(tǒng)的研發(fā)需要更為抽象的接口定義,以此達到更為合理的代碼復用。

Hello world

在了解泛型之前,先看一個簡單的加法函數(shù)定義

function add (x: number, y: number): number {
    return x + y;
}

當開發(fā)者想更多地使用此方法向外提供能力時,比如字符串的相加,此時就得對其進行改造,讓接受的參數(shù)和返回的值包含字符串類型??梢允褂?code>any來兼容多種類型,代碼如下:

function add (x: any, y: any): any {
    return x + y;
}

使用any固然可以達到所謂泛型的效果,可以支持多種類型值的傳入和返回,但此時也會丟失對類型的校驗能力。比如,傳入2個number類型的值進行加法運算,從樸素的認知出發(fā),應該返回number類型,但函數(shù)的這種定義形式只能返回any的類型。
typescript提供類型變量(type variables)可以解決上述的問題。類型變量作用于類型,而非實際的值,值的類型在使用時傳入。使用類型變量改造前面的代碼后,代碼如下:

function add<T> (x: T, y: T): T {
    return x + y;
}

add函數(shù)添加類型變量T,這樣就可以獲取到使用者傳入的實際類型,類型系統(tǒng)也能據(jù)此做相應的類型校驗。添加類型變量后,函數(shù)在調(diào)用時也需要傳入相應的類型,使用示例如下:

const result = add<number>(3, 4);

類型變量的傳入使用<>而不是()包裹,這里將number賦值給類型變量T,表明傳入?yún)?shù)和返回類型都是number。

Generic Types

函數(shù)的泛型定義類似于函數(shù)的定義,但與其不同的是沒有函數(shù)體,包含參數(shù)列表和返回類型的說明。比如將add賦值給另外一個定義的變量,代碼如下

const myAdd: (x: T, y: T) => T = add;

也可以使用其他類型變量名來替換T,跟定義函數(shù)時使用不同的參數(shù)名稱類似,比如將T換成InputType,代碼如下:

const myAdd: (x: InputType, y: InputType) => InputType = add;

函數(shù)泛型的定義還可以使用調(diào)用簽名的方式(call signature),形式類似于對象常量(object literal),代碼如下:

const myAdd: { <T>(x: T, y: T): T } = add;

調(diào)用簽名可以通過interface進行定義,這樣在一處定義,其他使用此類型的地方都可以引用,該定義如下:

interface GenericAdd {
    <T>(x: T, y: T):  T
}
// myAdd在使用時需要傳入相應的類型
const myAdd: GenericAdd = add;

在有些場景下,可以將類型變量T作為整個GenericAdd的類型變量,這樣整個interface內(nèi)部都可以使用這個類型變量,此時可以將類型變量提升作用域。代碼如下:

interface GenericAdd<T> {
     (x: T, y: T):  T
}
// myAdd的輸入和輸出類型均為number
const myAdd: GenericAdd<number> = add;

通過上面的修改,我們生成了一個generic interface。

Generic Classes

泛型類跟泛型接口形式類似,在類名后使用<>包裹類型變量,并在類中使用該類型變量。
這里可以直接看官方的文檔示例代碼,代碼如下:

class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

上述的代碼傳入number類型,當然也可以傳入string類型,代碼如下:

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = '';
stringNumeric.add = function (x, y) {
  return x + y;
};

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

不管是number還是string,這些都是簡單的類型,用戶同樣可以傳入更為復雜的自定義類型。

?? generic class中的靜態(tài)屬性不能應用類型變量。詳情參考這里

Generic Constraints

使用類型變量時,需要關(guān)注對類型使用的限制。以前面的add函數(shù)為例,假設在字符串相加的場景中需要判斷字符串的長度,傳入的字符串長度不能小于2,否則不執(zhí)行字符串加法。代碼可能如下:

function add<T> (x: T, y: T): T {
// Property 'length' does not exist on type 'Type'.
    if (x.length < 2 || y.length < 2) {
        return console.log('字符串的長度不能小于2');
    }
    return x + y;
}

在類型校驗階段,會拋出錯誤Property 'length' does not exist on type 'T'.。很明顯,number類型的值是沒有length屬性的。
假設該方法只處理具有length屬性類型的參數(shù)的加法,那么可以做相應的類型約束,讓T從包含length屬性的基礎(chǔ)類型繼承。代碼如下:

interface WithLength {
    length: number;
}

function add<T extends WithLength> (x: T, y: T): T {
    if (x.length < 2 || y.length < 2) {
        return console.log('長度不能小于2');
    }
    return x + y;
}

對類型添加約束后,函數(shù)調(diào)用時類型系統(tǒng)會根據(jù)約束校驗參數(shù)和返回值是否合法。

Using Type Parameters in Generic Constraints

受益于typescript提供的類型運算的能力,類型變量可以從其他類型變量生成而來。同樣來看官方文檔提供的獲取對象屬性的示例代碼,代碼如下:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
//?? m 不是 x 對象的屬性,編譯階段會報錯
getProperty(x, "m");

函數(shù)getProperty接收兩個類型變量TypeKey,其中Key是基于對Type的類型運算生成的。keyof是對給定類型的運算,如果Type為對象類型,則其結(jié)果為一個對象key組成的union。以上面的代碼為例:Key = 'a' | 'b' | 'c' | 'd'。

Using Class Types in Generics

在typescript中使用泛型構(gòu)建實例工廠,需要關(guān)注構(gòu)造函數(shù)的使用。如下代碼:

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

這里需要注意定義構(gòu)造函數(shù)的簽名,構(gòu)造函數(shù)返回的實例類型需給傳入的class類型一致。
下面是一個更為復雜的示例:

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

這個示例展示了如何使用prototype(通過classextend語法糖)約束類構(gòu)造函數(shù)和類類型之間的關(guān)系。

小結(jié)

本文介紹了泛型/泛型約束/泛型類及其簡單的使用示例,揭開typescript泛型神秘的面紗,讓讀者能否快速了解和使用泛型。同時本文也是typescript類型運算系列的第一篇,后續(xù)會帶來其他類型運算的文章,敬請期待。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容