泛型
軟件工程中黄鳍,我們不僅要?jiǎng)?chuàng)建定義良好且一致的 API边器,同時(shí)也要考慮可重用性。 組件不僅能夠支持當(dāng)前的數(shù)據(jù)類型碰逸,同時(shí)也能支持未來的數(shù)據(jù)類型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能阔加。
在像 C# 和 Java 這樣的語言中饵史,可以使用泛型來創(chuàng)建可重用的組件,一個(gè)組件可以支持多種類型的數(shù)據(jù)。 這樣用戶就可以以自己的數(shù)據(jù)類型來使用組件胳喷。
基礎(chǔ)示例
下面來創(chuàng)建第一個(gè)使用泛型的例子:identity
函數(shù)湃番。 這個(gè)函數(shù)會(huì)返回任何傳入它的值。 你可以把這個(gè)函數(shù)當(dāng)成是 echo
命令吭露。
不用泛型的話吠撮,這個(gè)函數(shù)可能是下面這樣:
function identity(arg: number): number {
return arg
}
或者,我們使用 any
類型來定義函數(shù):
function identity(arg: any): any {
return arg
}
使用 any
類型會(huì)導(dǎo)致這個(gè)函數(shù)可以接收任何類型的 arg
參數(shù)讲竿,但是這樣就丟失了一些信息:傳入的類型與返回的類型應(yīng)該是相同的泥兰。如果我們傳入一個(gè)數(shù)字,我們只知道任何類型的值都有可能被返回题禀。
因此鞋诗,我們需要一種方法使返回值的類型與傳入?yún)?shù)的類型是相同的。這里迈嘹,我們使用了類型變量削彬,它是一種特殊的變量,只用于表示類型而不是值秀仲。
function identity<T>(arg: T): T {
return arg
}
我們給 identity
添加了類型變量 T
融痛。 T
幫助我們捕獲用戶傳入的類型(比如:number
),之后我們就可以使用這個(gè)類型神僵。 之后我們?cè)俅问褂昧?T
當(dāng)做返回值類型⊙闼ⅲ現(xiàn)在我們可以知道參數(shù)類型與返回值類型是相同的了。這允許我們跟蹤函數(shù)里使用的類型的信息保礼。
我們把這個(gè)版本的 identity
函數(shù)叫做泛型安券,因?yàn)樗梢赃m用于多個(gè)類型。 不同于使用 any
氓英,它不會(huì)丟失信息,像第一個(gè)例子那像保持準(zhǔn)確性鹦筹,傳入數(shù)值類型并返回?cái)?shù)值類型铝阐。
我們定義了泛型函數(shù)后,可以用兩種方法使用铐拐。 第一種是徘键,傳入所有的參數(shù),包含類型參數(shù):
let output = identity<string>('myString')
這里我們明確的指定了 T
是 string
類型遍蟋,并做為一個(gè)參數(shù)傳給函數(shù)吹害,使用了 <>
括起來而不是 ()
。
第二種方法更普遍虚青。利用了類型推論 -- 即編譯器會(huì)根據(jù)傳入的參數(shù)自動(dòng)地幫助我們確定 T
的類型:
let output = identity('myString')
注意我們沒必要使用尖括號(hào)(<>
)來明確地傳入類型它呀;編譯器可以查看 myString
的值,然后把 T
設(shè)置為它的類型。 類型推論幫助我們保持代碼精簡和高可讀性纵穿。如果編譯器不能夠自動(dòng)地推斷出類型的話下隧,只能像上面那樣明確的傳入 T
的類型,在一些復(fù)雜的情況下谓媒,這是可能出現(xiàn)的淆院。
使用泛型變量
使用泛型創(chuàng)建像 identity
這樣的泛型函數(shù)時(shí),編譯器要求你在函數(shù)體必須正確的使用這個(gè)通用的類型句惯。 換句話說土辩,你必須把這些參數(shù)當(dāng)做是任意或所有類型。
看下之前 identity
例子:
function identity<T>(arg: T): T {
return arg
}
如果我們想打印出 arg
的長度抢野。 我們很可能會(huì)這樣做:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length)
return arg
}
如果這么做拷淘,編譯器會(huì)報(bào)錯(cuò)說我們使用了 arg
的 .length
屬性,但是沒有地方指明 arg
具有這個(gè)屬性蒙保。記住辕棚,這些類型變量代表的是任意類型,所以使用這個(gè)函數(shù)的人可能傳入的是個(gè)數(shù)字邓厕,而數(shù)字是沒有 .length
屬性的逝嚎。
現(xiàn)在假設(shè)我們想操作 T
類型的數(shù)組而不直接是 T
。由于我們操作的是數(shù)組详恼,所以 .length
屬性是應(yīng)該存在的补君。我們可以像創(chuàng)建其它數(shù)組一樣創(chuàng)建這個(gè)數(shù)組:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
你可以這樣理解 loggingIdentity
的類型:泛型函數(shù) loggingIdentity
,接收類型參數(shù) T
和參數(shù) arg
昧互,它是個(gè)元素類型是 T
的數(shù)組挽铁,并返回元素類型是T
的數(shù)組。 如果我們傳入數(shù)字?jǐn)?shù)組敞掘,將返回一個(gè)數(shù)字?jǐn)?shù)組叽掘,因?yàn)榇藭r(shí) T
的的類型為 number
。 這可以讓我們把泛型變量 T
當(dāng)做類型的一部分使用玖雁,而不是整個(gè)類型更扁,增加了靈活性。
泛型類型
上一節(jié)赫冬,我們創(chuàng)建了 identity
通用函數(shù)浓镜,可以適用于不同的類型。 在這節(jié)劲厌,我們研究一下函數(shù)本身的類型膛薛,以及如何創(chuàng)建泛型接口。
泛型函數(shù)的類型與非泛型函數(shù)的類型沒什么不同补鼻,只是有一個(gè)類型參數(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ì)象字面量來定義泛型函數(shù):
function identity<T>(arg: T): T {
return arg
}
let myIdentity: {<T>(arg: T): T} = identity
這引導(dǎo)我們?nèi)懙谝粋€(gè)泛型接口了增淹。我們把上面例子里的對(duì)象字面量拿出來做為一個(gè)接口:
interface GenericIdentityFn {
<T>(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn = identity
我們甚至可以把泛型參數(shù)當(dāng)作整個(gè)接口的一個(gè)參數(shù)椿访。 這樣我們就能清楚的知道使用的具體是哪個(gè)泛型類型(比如: Dictionary<string>
而不只是Dictionary
)。這樣接口里的其它成員也能知道這個(gè)參數(shù)的類型了虑润。
interface GenericIdentityFn<T> {
(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn<number> = identity
注意成玫,我們的示例做了少許改動(dòng)。 不再描述泛型函數(shù)拳喻,而是把非泛型函數(shù)簽名作為泛型類型一部分哭当。 當(dāng)我們使用 GenericIdentityFn
的時(shí)候,還得傳入一個(gè)類型參數(shù)來指定泛型類型(這里是:number
)冗澈,鎖定了之后代碼里使用的類型钦勘。對(duì)于描述哪部分類型屬于泛型部分來說,理解何時(shí)把參數(shù)放在調(diào)用簽名里和何時(shí)放在接口上是很有幫助的亚亲。
除了泛型接口彻采,我們還可以創(chuàng)建泛型類。 注意捌归,無法創(chuàng)建泛型枚舉和泛型命名空間肛响。
泛型類
泛型類看上去與泛型接口差不多。 泛型類使用( <>
)括起泛型類型惜索,跟在類名后面特笋。
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
類的使用是十分直觀的,并且你可能已經(jīng)注意到了巾兆,沒有什么去限制它只能使用 number
類型猎物。 也可以使用字符串或其它更復(fù)雜的類型。
let stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = function(x, y) {
return x + y
}
console.log(stringNumeric.add(stringNumeric.zeroValue, 'test'))
與接口一樣角塑,直接把泛型類型放在類后面蔫磨,可以幫助我們確認(rèn)類的所有屬性都在使用相同的類型。
我們?cè)?a href="/chapter2/class" target="_blank">類那節(jié)說過圃伶,類有兩部分:靜態(tài)部分和實(shí)例部分质帅。 泛型類指的是實(shí)例部分的類型,所以類的靜態(tài)屬性不能使用這個(gè)泛型類型留攒。
泛型約束
我們有時(shí)候想操作某類型的一組值,并且我們知道這組值具有什么樣的屬性嫉嘀。在 loggingIdentity
例子中炼邀,我們想訪問 arg
的 length
屬性,但是編譯器并不能證明每種類型都有 length
屬性剪侮,所以就報(bào)錯(cuò)了拭宁。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length)
return arg
}
相比于操作 any
所有類型洛退,我們想要限制函數(shù)去處理任意帶有 .length
屬性的所有類型。 只要傳入的類型有這個(gè)屬性杰标,我們就允許兵怯,就是說至少包含這一屬性。為此腔剂,我們需要列出對(duì)于 T
的約束要求媒区。
我們定義一個(gè)接口來描述約束條件,創(chuàng)建一個(gè)包含 .length
屬性的接口掸犬,使用這個(gè)接口和 extends
關(guān)鍵字來實(shí)現(xiàn)約束:
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length) // OK
return arg
}
現(xiàn)在這個(gè)泛型函數(shù)被定義了約束袜漩,因此它不再是適用于任意類型:
loggingIdentity(3); // Error
我們需要傳入符合約束類型的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3}) // OK
在泛型約束中使用類型參數(shù)
你可以聲明一個(gè)類型參數(shù)湾碎,且它被另一個(gè)類型參數(shù)所約束宙攻。 比如,現(xiàn)在我們想要用屬性名從對(duì)象里獲取這個(gè)屬性介褥。 并且我們想要確保這個(gè)屬性存在于對(duì)象 obj
上座掘,因此我們需要在這兩個(gè)類型之間使用約束。
function getProperty<T, K extends keyof T> (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