TypeScript:一個(gè)好泛型的價(jià)值
原文轉(zhuǎn)自:https://juejin.im/post/6878868818836488205
TypeScript:一個(gè)好泛型的價(jià)值
在軟件開發(fā)領(lǐng)域,我們總是致力于創(chuàng)建可復(fù)用的組件,架構(gòu)被設(shè)計(jì)為可適應(yīng)多種情境,并且我們始終在尋找一種即便在面臨未知情況時(shí),也能自動(dòng)讓邏輯正確行事的方法。
盡管在某些情境下可能并不總是易于做到甚或根本不可行,但我們心里總想找到能被復(fù)現(xiàn)并變?yōu)榭杀粦?yīng)付的通用算法的某些模式。所謂 泛型(Generics) 的概念就是該行為的另一個(gè)例子,只是,這次我們不訴諸宏大,而是在代碼層面的細(xì)枝末節(jié)中試圖找出并描繪上述的模式。
且聽我細(xì)細(xì)道來……
何為泛型?
泛型是種一旦理解就樂在其中的概念,所以讓我只是先從這樣描述它開始吧:
泛型之于類型(Types),猶類型之于變量也
換言之,泛型為你提供了一種不用指定特別某種類型就能使用若干類型的方式。這給你的函數(shù)定義、類型定義,甚至接口定義賦予了更高一層的靈活性。
用于解釋泛型威力的典型例子,莫過于 identity 函數(shù)。該函數(shù)本質(zhì)上只是原樣返回你傳入的唯一參數(shù),別無他用,但如果你思考一下,如何在一種強(qiáng)類型語言中定義這樣一個(gè)函數(shù)呢?
function?identity(value:?number):number?{
??return?value;
}
上面的函數(shù)對(duì)于數(shù)字工作良好,那字符串呢?或布爾值?自定義類型又如何?在 TypeScript 中要覆蓋所有可能性,明顯只能選擇 any 類型了:
function?identity(value:?any):?any?{
??return?value
}
這還挺行得通的,但此刻你的函數(shù)實(shí)際上丟失了所有類型的概念,你將不能在本該有確定類型信息的地方使用它們了。本質(zhì)上來說現(xiàn)在你可以傳入任何值而編譯器將一言不發(fā),這和你使用普通的 JavaScript 就沒有區(qū)別了(即無論怎樣都沒有類型信息了):
let?myVar?=?identity("Fernando")
console.log(myVar.length)?//?工作良好!
myVar?=?identity(23)
console.log(myVar.length)?//?也能工作,盡管打印了?"undefined"
現(xiàn)在因?yàn)闆]有類型信息了,編譯器就不能檢查和函數(shù)相關(guān)的任何事情以及變量了,如此一來我們運(yùn)行出了一個(gè)意外的 “undefined”(若將本例推及有更多邏輯復(fù)雜邏輯的真實(shí)場(chǎng)景將很可能變成最糟糕的一段程序)。
我們?cè)撊绾涡迯?fù)這點(diǎn)并避免使用 any 類型呢?
TypeScript 泛型來拯救
正如我曾 嘗試 說的那樣:一個(gè)泛型就像若干類型的一個(gè)變量,這意味著我們可以定義一個(gè)表示任何類型的變量,同時(shí)能保持住類型信息。后者是關(guān)鍵,因?yàn)槟钦?any 做不到的。基于這種想法,現(xiàn)在可以這樣重構(gòu)我們的 identity 函數(shù):
function?identity<T>(value:?T):?T?{
??return?value;
}
記住,用來表示泛型名字的可以是任意字母,你可以隨意命名。但使用一個(gè)單字母呢,看起來是個(gè)標(biāo)準(zhǔn)了,所以我們也從善如流。
這不單讓我們定義了一個(gè)可被任意類型使用的函數(shù),現(xiàn)在相關(guān)的變量也將保留你所選擇類型的正確信息。如下:

圖片中兩件事情值得注意:
我直接在函數(shù)名之后(在 \< 和 > 之間)指定了類型。在本例中,由于函數(shù)簽名足夠簡(jiǎn)單,我們其實(shí)可以省略這部分來調(diào)用函數(shù)而編譯器將會(huì)從所傳參數(shù)推斷出類型。然而,如果你把單詞 number改為string則整個(gè)例子將不再工作。現(xiàn)在無法打印出 length屬性了,因?yàn)閿?shù)字沒有這個(gè)屬性。
這正是你期待一個(gè)強(qiáng)類型語言該做的事情,并且這也是當(dāng)定義 通用的 行為時(shí)為何你要使用泛型的原因。
我還能用泛型做些什么?
前面的例子常被稱為泛型的 “Hello World”, 你能在任何一篇文章中找到它,但它是解釋泛型潛能的一個(gè)絕佳途徑。但還有些其他你能做到的有趣之事,當(dāng)然了總是在類型安全領(lǐng)域的,別忘了,你要構(gòu)建能在多種環(huán)境下復(fù)用的東西,同時(shí)還要努力保持住我們非常關(guān)心的類型信息。
自動(dòng)結(jié)構(gòu)檢查
泛型中的這一點(diǎn)無疑是我最喜歡的了。考慮如下場(chǎng)景:你有一個(gè)固定的結(jié)構(gòu)(即一個(gè)對(duì)象)并且你在試圖動(dòng)態(tài)地訪問其中一個(gè)屬性。我們之前已經(jīng)像這樣完成了這個(gè)功能:
function?get(obj,?prop)?{
??if(!obj[prop])?return?null;
??return?obj[prop]
}
我并沒有用到 hasOwnProperty 或其他類似的技術(shù),但你能明白要點(diǎn)就好,你需要執(zhí)行一個(gè)基礎(chǔ)的結(jié)構(gòu)檢查以確保能控制所訪問的屬性不屬于對(duì)象的情況。現(xiàn)在,讓我們將其轉(zhuǎn)換為類型安全的 TypeScript 并看看泛型能如何幫助我們:
type?Person?=?{
????name:?string,
????age:?number,
????city:?string
}
function?getPersonProp<K?extends?keyof?Person>(p:Person,?key:?K):?any?{
????return?p[key]
}
現(xiàn)在,請(qǐng)注意我是如何使用泛型符號(hào)的:我不是僅聲明了一個(gè)泛型 K,同時(shí)還說明了它 繼承自 Person 中的鍵類型。這太棒了!你可以聲明式的界定你傳入的值會(huì)匹配字符串 name、age 或 city。本質(zhì)上你聲明了一個(gè)枚舉值,而當(dāng)你這么想的時(shí)候,就沒之前那么興奮了吧。但你也不用止步于此,可以通過像這樣重新定義該函數(shù)來重燃激情:
function?get<T,?K?extends?keyof?T>(p:?T,?key:?K):?any?{
????return?p[key]
}
這就對(duì)了,我們現(xiàn)在有了兩個(gè)泛型,后一個(gè)被聲明為繼承自前一個(gè)中的鍵,但本質(zhì)上的好處是你現(xiàn)在不再受限于某一種具體類型(即 Person 類型的對(duì)象) 了,該函數(shù)可被你放心大膽地用于任何類型或結(jié)構(gòu)了。
下面是當(dāng)你用一個(gè)非法屬性名使用它時(shí)將會(huì)發(fā)生的:

泛型類(Generic classes)
泛型不僅應(yīng)用于函數(shù)簽名,亦可用來定義你自己的泛型類。這提供了將通用邏輯封裝進(jìn)可復(fù)用構(gòu)造中的能力,讓一些有意思的行為變得可能。
下面是一個(gè)例子:
abstract?class?Animal?{
????handle()?{?throw?new?Error("Not?implemented")?}
}
class?Horse?extends?Animal{
????color:?string
????handle()?{
????????console.log("Riding?the?horse...")
????}
}
class?Dog?extends?Animal{
????name:?string?
????handle()?{
????????console.log("Feeding?the?dog...")
????}
}
class?Handlerextends?Animal>?{
????animal:?T
????constructor(animal:?T)?{
????????this.animal?=?animal
????}
????handle()?{
????????this.animal.handle()
????}
}
class?DogHandler?extends?Handler?{}
class?HorseHandler?extends?Handler?{}
在本例中,我們定義了一個(gè)可以處理任意動(dòng)物類型的處理類,雖說不用泛型也能做到,但使用泛型的益處在最后兩行顯而易見。這是因?yàn)榻柚盒停幚眍愡壿嬐耆环庋b進(jìn)了一個(gè)泛型類中,從而我們可以約束類型并創(chuàng)建指定類型的類,這樣的類只對(duì)動(dòng)物類型生效。你也可以在此添加額外的行為,而類型信息也得以保留。
來自這個(gè)例子的另一個(gè)收獲是,泛型可被約束為僅繼承自指定的一組類型。正如你所見,T 只能是 Dog 或 Horse 而非其他。
可變參數(shù)元組(Variadic Tuples)
實(shí)際上這是 TypeScript 4.0 中的新特性。并且盡管我 [已經(jīng)在這篇文章中介紹了它][Link 2],此處仍會(huì)快速回顧一下。
概況來說,可變參數(shù)元組帶來的,是用泛型定義某元組中一個(gè)可變的部分,默認(rèn)情況下這部分什么都沒有。
一個(gè)普通的元組定義將產(chǎn)生一個(gè)固定尺寸的數(shù)組,其所有元素都是預(yù)定義好的類型:
type?MyTuple?=?[string,?string,?number]
let?myList:MyTuple?=?["Fernando",?"Doglio",?37]
現(xiàn)在,歸功于泛型和可變參數(shù)元組,你可以這樣做:
type?MyTupleextends?unknown[]>?=?[string,?string,?...T,?number]
let?myList:MyTuple<[boolean,?number]>?=?["Fernando",?"Doglio",?true,?3,?37]
let?myList:MyTuple<[number,?number,?number]>?=?["Fernando",?"Doglio",?1,2,3,4]
如果你注意看,我們使用了一個(gè)泛型 T(繼承自一個(gè) unknown 數(shù)組)用以將一個(gè)可變部分置于元組中。因?yàn)?T 是 unknown 類型的一個(gè)列表,你可以在里面裝任何東西。比分說,你可以將其定義為單一類型的一個(gè)列表,就像這樣:
type?anotherTuple?=?[boolean,?...T,?boolean];
let?oneNumber:?anotherTuple<[number]>?=?[true,?1,?true];
let?twoNumbers:?anotherTuple<[number,?number]>?=?[true,?1,?2,?true]
let?manyNumbers:?anotherTuple<[number,?number,?number,?number]>?=?[true,?1,?2,?3,?4,?true]
天高任鳥飛,本質(zhì)上你可以定義出一種模板元組的形式,以供稍后隨意(當(dāng)然要按照你設(shè)置的模板)使用。
總結(jié)
泛型是一種非常強(qiáng)大的工具,雖然有時(shí)閱讀由其編寫的代碼宛如天書,但熟能生巧。慢慢品味,用心閱讀,你將看到其內(nèi)在的潛能。
推薦閱讀
