轉(zhuǎn)載請注明出處:葡萄城官網(wǎng),葡萄城為開發(fā)者提供專業(yè)的開發(fā)工具、解決方案和服務(wù),賦能開發(fā)者。
原文出處:https://blog.bitsrc.io/understanding-generics-in-typescript-1c041dc37569創(chuàng)新互聯(lián)建站長期為1000+客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為孟州企業(yè)提供專業(yè)的成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè),孟州網(wǎng)站改版等技術(shù)服務(wù)。擁有十年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。
本文介紹TypeScript中泛型(Generics)的概念和用法,它為什么重要,及其使用場景。我們會以一些清晰的例子,介紹其語法,類型和如何構(gòu)建參數(shù)。你可以在你的集成開發(fā)環(huán)境中跟著實踐。
要從本文中跟著學(xué)習(xí)的話,你需要在電腦上準(zhǔn)備以下東西:
安裝Node.js:你可以運行命令行檢查Node是否安裝好了。
1 | node -v |
安裝Node Package Manager: 通常安裝Node時,它會順帶安裝好所需版本的NPM。
安裝TypeScript:如果你安裝好了Node Package Manager,你可以用以下命令在本機(jī)的全局環(huán)境安裝TypeScript。
1 | npm install -g typescript |
集成開發(fā)環(huán)境:本文將使用微軟團(tuán)隊開發(fā)的Visual Studio Code。可以在這里下載。進(jìn)入其下載的目錄,并按照提示進(jìn)行安裝。記得選擇“添加打開代碼”(Add open with code)選項,這樣你就可以在本機(jī)從任何位置輕松打開VS Code了。
本文是寫給各層次的TypeScript開發(fā)人員的,包括但并不只是初學(xué)者。 這里給出了設(shè)置工作環(huán)境的步驟,是為了照顧那些TypeScript和Visual Studio Code的新手們。
在TypeScript中,泛型是一種創(chuàng)建可復(fù)用代碼組件的工具。這種組件不只能被一種類型使用,而是能被多種類型復(fù)用。類似于參數(shù)的作用,泛型是一種用以增強(qiáng)類(classes)、類型(types)和接口(interfaces)能力的非??煽康氖侄?。這樣,我們開發(fā)者,就可以輕松地將那些可復(fù)用的代碼組件,適用于各種輸入。然而,不要把TypeScript中的泛型錯當(dāng)成any
類型來使用——你會在后面看到這兩者的不同。
類似C#和Java這種語言,在它們的工具箱里,泛型是創(chuàng)建可復(fù)用代碼組件的主要手段之一。即,用于創(chuàng)建一個適用于多種類型的代碼組件。這允許用戶以他們自己的類使用該泛型組件。
在計算機(jī)中創(chuàng)建一個新文件夾,然后使用VS Code 打開它(如果你跟著從頭開始操作,那你已經(jīng)安裝好了)。
在VS Code中,創(chuàng)建一個app.ts
文件。我的TypeScript代碼都會放在這里面。
把下面打日志的代碼拷貝到編輯器中:
1 | console.log( "hello TypeScript" ); |
按下F5
鍵,你會看到一個像這樣的launch.json
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | { ?? // Use IntelliSense to learn about possible attributes. ?? // Hover to view descriptions of existing attributes. ?? // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 ?? "version" :? "0.2.0" , ?? "configurations" : [ ???? { ?????? "type" :? "node" , ?????? "request" :? "launch" , ?????? "name" :? "TypeScript" , ?????? "program" :? "${workspaceFolder}\\app.ts" , ?????? "outFiles" : [ ???????? "${workspaceFolder}/**/*.js" ?????? ] ???? } ?? ] } |
里面的name
字段的值,本來是Launch Program
,我把它改成了TypeScript
。你可以把它改成其他值。
點擊Terminal Tab
,選擇Run Tasks
,再選擇一個Task Runner
:"TypeScript Watch Mode",然后會彈出一個tasks.json
文件,把它改成下面像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | { ? // See https://go.microsoft.com/fwlink/?LinkId=733558 ? // for the documentation about the tasks.json format ? "version" :? "2.0.0" , ? "tasks" : [ ? { ?? "label" :? "echo" , ?? "type" :? "shell" , ?? "command" :? "tsc" , ?? "args" : [ "-w" ,? "-p" , "." ], ?? "problemMatcher" : [ ??? "$tsc-watch" ??? ], ?? "isBackground" :? true ?? } ? ] } |
在app.ts
所在的目錄,創(chuàng)建另一個文件tsconfig.json
。把下面的代碼拷貝進(jìn)去:
1 2 3 4 5 | { ?? "compilerOptions" : { ???? "sourceMap" :? true ?? } } |
這樣,Task Runner
就可以把TypeScript編譯成JavaScript,并且可監(jiān)聽到文件的變化,實時編譯。
再次點擊Ternimal
標(biāo)簽,選擇Run Build Task
,再選擇tsc: watch - tsconfig.json
,可以看到終端出現(xiàn)的信息:
1 | [21:41:31] Starting compilation? in ?watch mode… |
你可以使用VS Code的調(diào)試功能編譯TypeScript文件。
設(shè)置好了開發(fā)環(huán)境,你就可以著手處理TypeScript泛型概念相關(guān)的問題了。
TypeScript中不建議使用any
類型,原因有幾點,你可以在本文看到。其中一個原因,就是調(diào)試時缺乏完整的信息。而選擇VS Code作為開發(fā)工具的一個很好的理由,就是它帶來的基于這些信息的智能感知。
如果你有一個類,存儲著一個集合。有方法向該集合里添加?xùn)|西,也有方法通過索引獲取集合里的東西。像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 | class ?Collection { ?? private ?_things: string[]; ?? constructor() { ???? this ._things = []; ?? } ?? add(something: string) { ???? this ._things.push(something); ?? } ?? get(index: number): string { ???? return ?this ._things[index]; ?? } } |
你可以很快辨識出,此集合被顯示定義為一個string
類型的集合,顯然是不能在其中使用number
的。如果想要處理number
的話,可以創(chuàng)建一個接受number
而不是string
的集合。著是一個不錯的選擇,但有一個很大的缺點——代碼重復(fù)。代碼重復(fù),最終會導(dǎo)致編寫和調(diào)試代碼的時間增多,并且降低內(nèi)存的使用效率。
另一個選擇,是使用any
類型代替string
類型定義剛才的類,像下面這樣:
1 2 3 4 5 6 7 8 9 10 11 12 | class ?Collection { ?? private ?_things: any[]; ?? constructor() { ???? this ._things = []; ?? } ?? add(something: any) { ???? this ._things.push(something); ?? } ?? get(index: number): any { ???? return ?this ._things[index]; ?? } } |
此時,該集合支持你給出的任何類型。如果你創(chuàng)建像這樣的邏輯構(gòu)建此集合的話:
1 2 3 | let ?Stringss =? new ?Collection(); Stringss.add( "hello" ); Stringss.add( "world" ); |
這添加了字符串"hello"和"world"到集合中,你可以打出像length
這樣的屬性,返回任意一個集合元素的長度。
1 | console.log(Stringss.get(0).length); |
字符串"hello"有五個字符,運行TypeScript代碼,你可以在調(diào)試模式下看到它。
請注意,當(dāng)你鼠標(biāo)懸停在length屬性上時,VS Code的智能感知沒有提供任何信息,因為它不知道你選擇使用的確切類型。當(dāng)你像下面這樣,把其中一個添加的元素修改為其他類型時,比如number
,這種不能被智能感知到的情況會體現(xiàn)得更加明顯:
1 2 3 4 | let ?Strings =? new ?Collection(); Strings.add(001); Strings.add( "world" ); console.log(Strings.get(0).length); |
你打出一個undefined
的結(jié)果,仍然沒有什么有用信息。如果你更進(jìn)一步,決定打印string
的子字符串——它會報運行時錯誤,但不指不出任何具體的內(nèi)容,更重要的是,編譯器沒有給出任何類型不匹配的編譯時錯誤。
1 | console.log(Stringss.get(0).substr(0,1)); |
這僅僅是使用any
類型定義該集合的一種后果罷了。
剛才使用any
類型導(dǎo)致的問題,可以用TypeScript中的泛型來解決。其中心思想是類型安全。使用泛型,你可以用一種編譯器能理解的,并且合乎我們判斷的方式,指定類、類型和接口的實例。正如在其他強(qiáng)類型語言中的情況一樣,用這種方法,就可以在編譯時發(fā)現(xiàn)你的類型錯誤,從而保證了類型安全。
泛型的語法像這樣:
1 2 3 | function ?identity ?? return ?arg; } |
你可以在之前創(chuàng)建的集合中使用泛型,用尖括號括起來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class ?Collection ?? private ?_things: T[]; ?? constructor() { ???? this ._things = []; ?? } ?? add(something: T): void { ???? this ._things.push(something); ?? } ?? get(index: number): T { ???? return ?this ._things[index]; ?? } } let ?Stringss =? new ?Collection Stringss.add(001); Stringss.add( "world" ); console.log(Stringss.get(0).substr(0, 1)); |
如果將帶有尖括號的新邏輯復(fù)制到代碼編輯器中,你會立即注意到"001"下的波浪線。這是因為,TypeScript現(xiàn)在可以從指定的泛型類型推斷出001不是字符串。在T
出現(xiàn)的地方,就可以使用string
類型,這就實現(xiàn)了類型安全。本質(zhì)上,這個集合的輸出可以是任何類型,但你指明了它應(yīng)該是string
類型,所以編譯器推斷它就是string
類型。這里使用的泛型聲明是在類級別,它也可以在其他級別定義,如靜態(tài)方法級別和實例方法級別,你稍后會看到。
你可以在泛型聲明中,包含多個類型參數(shù),它們只需要用逗號分隔,像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 | class ?Collection ?? private ?_things: K[]; ?? constructor() { ???? this ._things = []; ?? } ?? add(something: K): void { ???? this ._things.push(something); ?? } ?? get(index: number): T { ???? console.log(index); ?? } } |
聲明時,類型參數(shù)也可以在函數(shù)中顯式使用,比如:
?
1 2 3 4 5 6 7 8 9 10 11 12 | class ?Collection { ?? private ?_things: any[]; ?? constructor() { ???? this ._things = []; ?? } ???? this ._things.push(something); ?? } ?? get(index: number): B { ???? return ?this ._things[index]; ?? } } |
因此,當(dāng)你要創(chuàng)建一個新的集合時,在方法級別聲明的泛型,現(xiàn)在也會在方法調(diào)用級別中被指示,像這樣:
1 2 3 | let ?Stringss =? new ?Collection(); Stringss.add "hello" ); Stringss.add( "world" ); |
你還可注意到,在鼠標(biāo)懸停時,VS Code智能感知能夠推斷出第二個add函數(shù)調(diào)用仍然是string
類型。
泛型聲明同樣適用于靜態(tài)方法:
1 2 3 | static ?add(something: A): void { ?? _things.push(something); } |
雖然初始化靜態(tài)方法時,可使用泛型類型,但是,對初始化靜態(tài)屬性則不能。
現(xiàn)在,你已經(jīng)對泛型有比較好的認(rèn)識,是時候提到泛型的核心缺點及其實用的解決方案了。使用泛型,許多屬性的類型都能被TypeScript推斷出來,然而,在某些TypeScript不能做出準(zhǔn)確推斷的地方,它不會做任何假設(shè)。為了類型安全,你需要將這些要求或者約束定義為接口,并在泛型初始化中繼承它們。
如果你有這樣一個非常簡單的函數(shù):
1 2 3 4 5 | function ?printName ?? console.log(arg.length); ?? return ?arg; } printName(3); |
因為TypeScript無法推斷出arg
參數(shù)是什么類型,不能證明所有類型都具有length
屬性,因此不能假設(shè)它是一個字符串(具有length
屬性)。所以,你會在length
屬性下看到一條波浪線。如前所述,你需要創(chuàng)建一個接口,讓泛型的初始化可以繼承它,以便編譯器不再報警。
1 2 3 | interface ?NameArgs { ?? length: number; } |
你可以在泛型聲明中繼承它:
1 2 3 4 | function ?printName ?? console.log(arg.length); ?? return ?arg; } |
這告訴TypeScript,可使用任何具有length
屬性的類型。 定義它之后,函數(shù)調(diào)用語句也必須更改,因為它不再適用于所有類型。 所以它應(yīng)看起來是這樣:
?
1 | printName({length: 1, value: 3}); |
這是一個很基礎(chǔ)的例子。但理解了它,你就能看到在使用泛型時,設(shè)置泛型約束是多么有用。
一個活躍于Stack Overflow社區(qū)的成員,Behrooz,在后續(xù)內(nèi)容中很好的回答了這個問題。在TypeScript中使用泛型的主要原因是使類型,類或接口充當(dāng)參數(shù)。 它幫助我們?yōu)椴煌愋偷妮斎胫赜孟嗤拇a,因為類型本身可用作參數(shù)。
泛型的一些好處有:
定義輸入和輸出參數(shù)類型之間的關(guān)系。比如
1 2 3 | function ?test ?? //… } |
允許你確保輸入和輸出使用相同的類型,盡管輸入是用的數(shù)組。
可使用編譯時更強(qiáng)大的類型檢查。在上訴示例中,編譯器讓你知道數(shù)組方法可用于輸入,任何其他方法則不行。
你可以去掉不需要的強(qiáng)制類型轉(zhuǎn)換。比如,如果你有一個常量列表:
1 | Array |
變量數(shù)組時,你可以由智能感知訪問到Item類型的所有成員。
官方文檔
你已經(jīng)看完了泛型概念的概述,并看到了各種示例來幫助揭示它背后的思想。 起初,泛型的概念可能令人困惑,我建議,把本文再讀一遍,并查閱本文所提供的額外資源,幫助自己更好地理解。泛型是一個很棒的概念,可以幫助我們在JavaScript中,更好地控制輸入和輸出。請快樂地編碼吧!