TypeScript 之泛型

picture

介紹

軟件工程中爪瓜,我們不僅要?jiǎng)?chuàng)建一致的定義良好的 API危纫,同時(shí)也要考慮可重用性而克。組件不僅能夠支持當(dāng)前的數(shù)據(jù)類(lèi)型溢吻,同樣也能支持未來(lái)的數(shù)據(jù)類(lèi)型维费,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供例了十分靈活的功能。

在像 C#Java 這樣的語(yǔ)言中,可以使用泛型來(lái)創(chuàng)建可重用的組件犀盟,一個(gè)組件可以支持多種類(lèi)型的數(shù)據(jù)而晒。這樣用戶(hù)就可以以自己的數(shù)據(jù)類(lèi)型來(lái)使用組件。


泛型之 Hello World

下面來(lái)創(chuàng)建第一個(gè)使用泛型的例子:identity 函數(shù)阅畴。這個(gè)函數(shù)會(huì)返回傳入傳入它的值倡怎。你可以把這個(gè)函數(shù)當(dāng)成是 echo 命令。

不用泛型的話(huà)恶阴,這個(gè)函數(shù)可能是下面這樣:

function identity(arg: number): number {
  return arg;
}

或者诈胜,我們使用 any 類(lèi)型來(lái)定義函數(shù):

function identity(arg: any): any {
  return arg;
}

使用 any 類(lèi)型會(huì)導(dǎo)致這個(gè)函數(shù)可以接收任何類(lèi)型的 arg 參數(shù),這樣就丟失了一些信息:傳入的類(lèi)型與返回的類(lèi)型應(yīng)該是相同的冯事。如果我么傳入一個(gè)數(shù)字焦匈,我們值知道任何類(lèi)型的值都有可能被返回。

因此昵仅,我們需要一種方法使返回值的類(lèi)型與傳入?yún)?shù)的類(lèi)型是相同的缓熟。這里,我們使用了 類(lèi)型變量 摔笤,它是一種特殊的變量够滑,只用于表示類(lèi)型而不是值。

function identity<T>(arg: T): T {
  return arg;
}

我們給 identity 添加了類(lèi)型變量 T吕世。 T 幫助我們捕獲用戶(hù)傳入的類(lèi)型(比如:number)彰触,之后我們就可以使用這個(gè)類(lèi)型。 之后我們?cè)俅问褂昧?T 當(dāng)做返回值類(lèi)型∶剑現(xiàn)在我們可以知道參數(shù)類(lèi)型與返回值類(lèi)型是相同的了况毅。 這允許我們跟蹤函數(shù)里使用的類(lèi)型的信息。

我們把這個(gè)版本的 identity 函數(shù)叫做泛型尔艇,因?yàn)樗梢赃m用于多個(gè)類(lèi)型尔许。不同于使用 any,它不會(huì)丟失信息终娃,像第一個(gè)例子那樣保持準(zhǔn)確性味廊,傳入數(shù)值類(lèi)型并返回?cái)?shù)值類(lèi)型。

我們定義了泛型函數(shù)后棠耕,可以用兩種方法使用:
第一種是余佛,傳入所有的參數(shù),包含類(lèi)型參數(shù):

let output = identity<string>('myString'); // type of output will be 'string'

這里我們明確的指定了 Tstring 類(lèi)型窍荧,并做為一個(gè)參數(shù)傳給函數(shù)辉巡,使用了 <> 括起來(lái)而不是 ()

第二種方法更普遍搅荞。利用了類(lèi)型推斷 -- 即編譯器會(huì)根據(jù)傳入的參數(shù)自動(dòng)地幫助我們確定 T 的類(lèi)型:

let output = identity('myString'); // type of output will be 'string'

注意我們沒(méi)必要使用 <> 來(lái)明確地傳入類(lèi)型;編譯器可以查看 myString 的值,然后把 T 設(shè)置為它的類(lèi)型咕痛。類(lèi)型推斷幫助我們保持代碼精簡(jiǎn)和高可讀性痢甘。如果編譯器不能夠自動(dòng)地推斷出類(lèi)型的話(huà),只能像上面那樣明確的傳入 T 的類(lèi)型茉贡,在一些復(fù)雜的情況下塞栅,這是可能出現(xiàn)的。


使用泛型變量

使用泛型創(chuàng)建像 identity 這樣的泛型函數(shù)時(shí)腔丧,編譯器要求你在函數(shù)體內(nèi)必須正確的使用這個(gè)通用的類(lèi)型放椰。換句話(huà)說(shuō),你必須把這些參數(shù)當(dāng)做是任意或所有類(lèi)型愉粤。

看下之前 identity 例子:

function identity<T>(arg: T): T {
  return arg;
}

如果我們想同時(shí)打印出 arg 的長(zhǎng)度砾医。 我們很可能會(huì)這樣做:

function loggingIdentity<T>(arg: T) {
  console.log(arg.length); // Error:T doesn't have .length
  return arg;
}

你可以這樣理解 loggingIdentity 的類(lèi)型:泛型函數(shù) loggingIdentity ,接收類(lèi)型參數(shù) T 和參數(shù) arg 衣厘,它是個(gè)元素類(lèi)型是 T 的數(shù)組如蚜,并返回元素類(lèi)型是 T 的數(shù)組。如果我們傳入數(shù)字?jǐn)?shù)組影暴,將返回一個(gè)數(shù)字?jǐn)?shù)組错邦,因?yàn)榇藭r(shí) T 的類(lèi)型為 number 。這可以讓我們把泛型變量 T 當(dāng)作類(lèi)型的一部分使用型宙,而不是整個(gè)類(lèi)型撬呢,增加了靈活性。

我們也可以這樣實(shí)現(xiàn)上面的例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}

使用過(guò)其他語(yǔ)言妆兑,你可能對(duì)這種語(yǔ)法已經(jīng)很熟悉了魂拦。在下一節(jié),會(huì)介紹如何創(chuàng)建自定義泛型像 T 一樣箭跳。


泛型類(lèi)型

上一節(jié)晨另,我們創(chuàng)建了 identity 通用函數(shù),可以適用于不同的類(lèi)型谱姓。在這節(jié)借尿,我們研究一下函數(shù)本身的類(lèi)型,以及如何讓創(chuàng)建泛型接口屉来。
泛型函數(shù)的類(lèi)型與非泛型函數(shù)的類(lèi)型沒(méi)有什么不同路翻,只是有一個(gè)類(lèi)型參數(shù)在最前面,像函數(shù)聲明一樣:

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

我們也可以使用不同的泛型參數(shù)名茄靠,只要在數(shù)量上和使用方式上能對(duì)應(yīng)上就可以茂契。

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

我們還可以使用帶有調(diào)用簽名的對(duì)象字面量來(lái)定義泛型函數(shù):

function indetity<T>(arg: T): T {
  return arg;
}

let myIdentity: { <T>(arg: T): T } = identity;

這引導(dǎo)我們?nèi)?xiě)第一個(gè)泛型接口了。我們把上面例子里的對(duì)象字面量拿出來(lái)做為一個(gè)接口:

interface GenericIdentityFn {
  <T>(arg: T): T;
}

function identitty<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentity = identity;

一個(gè)相似的例子慨绳,我們可能想把泛型參數(shù)當(dāng)作整個(gè)接口的一個(gè)參數(shù)掉冶。這樣我們就能清楚的知道使用的具體是那個(gè)泛型類(lèi)型(比如:Dictionary<string> 而不是 Dictionary)真竖。這樣接口里的其它成員也能知道這個(gè)參數(shù)的類(lèi)型了。

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

注意厌小,我們的示例做了少許改動(dòng)恢共。不再描述泛型函數(shù),而是把非泛型函數(shù)簽名作為泛型類(lèi)型的一部分璧亚。當(dāng)我們使用 GenericIdentityFn 的時(shí)候讨韭,還得傳入一個(gè)類(lèi)型參數(shù)來(lái)指定泛型類(lèi)型(這里是:number),鎖定了之后代碼里使用的類(lèi)型癣蟋。對(duì)于描述哪些部分屬于泛型部分來(lái)說(shuō)透硝,理解何時(shí)把參數(shù)放在調(diào)用簽名里和何時(shí)放在接口上是很有幫助的。

除了泛型接口疯搅,我們還可以創(chuàng)建泛型類(lèi)濒生。注意,無(wú)法創(chuàng)建泛型枚舉和泛型命名空間秉撇。


泛型類(lèi)

泛型類(lèi)看上去與泛型接口差不多甜攀。泛型類(lèi)使用(<>)擴(kuò)起泛型類(lèi)型,跟在類(lèi)名后面琐馆。

class GenericNumber<T> {
  zeroValue: T;
  add(x: T, y: T) => T;
}

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

GenericNumber 類(lèi)的使用是十分直觀的规阀,并且你可能已經(jīng)注意到了,沒(méi)有什么去限制它只能使用 number 類(lèi)型瘦麸。 也可以使用字符串或其它更復(fù)雜的類(lèi)型谁撼。

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

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

與接口一樣,直接把泛型類(lèi)型放在類(lèi)后面滋饲,可以幫助我們確認(rèn)類(lèi)的所有屬性都在使用相同的類(lèi)型厉碟。

我們?cè)?類(lèi) 那節(jié)說(shuō)過(guò),類(lèi)有兩部分:靜態(tài)部分和實(shí)例部分屠缭。泛型類(lèi)指的是實(shí)例部分的類(lèi)型箍鼓,所以類(lèi)的靜態(tài)屬性不能使用泛型類(lèi)型。


泛型約束

你應(yīng)該會(huì)記得之前的一個(gè)例子呵曹,我們有時(shí)候想操作某類(lèi)型的一組值款咖,并且我們知道這組值具有什么樣的屬性。在 loggingIdentity 例子中奄喂,我們想訪(fǎng)問(wèn) arglengrh 屬性铐殃,但是編譯器并不能證明每種類(lèi)型都有 length 屬性,所以就報(bào)錯(cuò)了跨新。

function loggingIdentity<T>(arg: T): T {
  console.log(arr.length); // Error: T doesn't have .length
  return arg;
}

相比于操作 any 所有類(lèi)型富腊,我們想要限制函數(shù)去處理任意帶有 .length 屬性的所有類(lèi)型。只要傳入的類(lèi)型有這個(gè)屬性域帐,我們就允許赘被,就是說(shuō)至少包含這一屬性是整。為此,我們需要列出對(duì)于 T 的約束要求。

為此逼侦,我們定義一個(gè)接口來(lái)描述約束條件。創(chuàng)建一個(gè)包含 .length 屬性的接口,使用這個(gè)接口和 extends 關(guān)鍵字來(lái)實(shí)現(xiàn)約束:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // Now we konw it has a .length property, so no more error
  return arg;
}

現(xiàn)在這個(gè)泛型函數(shù)被定義了約束草雕,因此它不再是適用于任意類(lèi)型:

loggingIdentity(3); // number doesn't have a .length property

我們需要傳入符合約束類(lèi)型的值,必須包含必須的屬性:

loggingIdentity({ length: 10, value: 3 });

在泛型約束中使用類(lèi)型參數(shù)

你可以聲明一個(gè)類(lèi)型參數(shù)教翩,且它被另一個(gè)類(lèi)型函數(shù)所約束逮刨。比如,現(xiàn)在我們想要用屬性名從對(duì)象里獲取這個(gè)屬性瓦糟,并且我們想要確保這個(gè)屬性存在于對(duì)象 obj 上筒愚,因此我們需要在這兩個(gè)類(lèi)型之間使用約束。

function getProperty(obj: T, key: K) {
  return obj[key];
}

let x = {
  a: 1,
  b: 2,
  c: 3,
  d: 4
};

getProperty(x, 'a'); // okay
getProperty(x, 'm'); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型里使用類(lèi)類(lèi)型

TypeScript 使用泛型創(chuàng)建工廠函數(shù)時(shí)菩浙,需要引用構(gòu)造函數(shù)的類(lèi)類(lèi)型巢掺。比如:

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

一個(gè)更高級(jí)的例子,使用原型屬性推斷并約束構(gòu)造函數(shù)與類(lèi)實(shí)例的關(guān)系劲蜻。

class BeeKeeper {
  hasMask: boolean;
}

class ZooKeeper {
  nametag: string;
}

class Animal {
  numLegs: number;
}

class Bee extends Animal {
  keeper: BeeKeeper;
}

class Lion extends Ainmal {
  keeper: ZooKeeper;
}

function createInstance<A extends Ainmal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!

本文參考來(lái)源: TypeScript 泛型

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載陆淀,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末先嬉,一起剝皮案震驚了整個(gè)濱河市轧苫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疫蔓,老刑警劉巖含懊,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異衅胀,居然都是意外死亡岔乔,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)滚躯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雏门,“玉大人,你說(shuō)我怎么就攤上這事哀九〗伺洌” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵阅束,是天一觀的道長(zhǎng)呼胚。 經(jīng)常有香客問(wèn)我,道長(zhǎng)息裸,這世上最難降的妖魔是什么蝇更? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任沪编,我火速辦了婚禮,結(jié)果婚禮上年扩,老公的妹妹穿的比我還像新娘蚁廓。我一直安慰自己,他們只是感情好厨幻,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布相嵌。 她就那樣靜靜地躺著,像睡著了一般况脆。 火紅的嫁衣襯著肌膚如雪饭宾。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,785評(píng)論 1 290
  • 那天格了,我揣著相機(jī)與錄音看铆,去河邊找鬼。 笑死盛末,一個(gè)胖子當(dāng)著我的面吹牛弹惦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播悄但,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼棠隐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了檐嚣?” 一聲冷哼從身側(cè)響起宵荒,我...
    開(kāi)封第一講書(shū)人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎净嘀,沒(méi)想到半個(gè)月后报咳,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挖藏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年暑刃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膜眠。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡岩臣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宵膨,到底是詐尸還是另有隱情架谎,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布辟躏,位于F島的核電站谷扣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏捎琐。R本人自食惡果不足惜会涎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一裹匙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧末秃,春花似錦概页、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至铃将,卻和暖如春徽曲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背麸塞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涧衙,地道東北人哪工。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像弧哎,于是被迫代替她去往敵國(guó)和親雁比。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348