十分鐘教你理解TypeScript中的泛型
轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
原文出處:https://blog.bitsrc.io/understanding-generics-in-typescript-1c041dc37569
你將在本文中學到什麼
本文介紹TypeScript中泛型(Generics)的概念和用法,它為什麼重要,及其使用場景。我們會以一些清晰的例子,介紹其語法,型別和如何構建引數。你可以在你的整合開發環境中跟著實踐。
準備工作
要從本文中跟著學習的話,你需要在電腦上準備以下東西:
- 安裝Node.js:你可以執行命令列檢查Node是否安裝好了。
node -v
- 安裝Node Package Manager: 通常安裝Node時,它會順帶安裝好所需版本的NPM。
- 安裝TypeScript:如果你安裝好了Node Package Manager,你可以用以下命令在本機的全域性環境安裝TypeScript。
-
npm install -g typescript
整合開發環境:本文將使用微軟團隊開發的Visual Studio Code。可以在這裡下載。進入其下載的目錄,並按照提示進行安裝。記得選擇“新增開啟程式碼”(Add open with code)選項,這樣你就可以在本機從任何位置輕鬆開啟VS Code了。
本文是寫給各層次的TypeScript開發人員的,包括但並不只是初學者。 這裡給出了設定工作環境的步驟,是為了照顧那些TypeScript和Visual Studio Code的新手們。
TypeScript裡的泛型是個啥
在TypeScript中,泛型是一種建立可複用程式碼元件的工具。這種元件不只能被一種型別使用,而是能被多種型別複用。類似於引數的作用,泛型是一種用以增強類(classes)、型別(types)和介面(interfaces)能力的非常可靠的手段。這樣,我們開發者,就可以輕鬆地將那些可複用的程式碼元件,適用於各種輸入。然而,不要把TypeScript中的泛型錯當成any
類似C#和Java這種語言,在它們的工具箱裡,泛型是建立可複用程式碼元件的主要手段之一。即,用於建立一個適用於多種型別的程式碼元件。這允許使用者以他們自己的類使用該泛型元件。
在VS Code中配置TypeScript
在計算機中建立一個新資料夾,然後使用VS Code 開啟它(如果你跟著從頭開始操作,那你已經安裝好了)。
在VS Code中,建立一個app.ts
檔案。我的TypeScript程式碼都會放在這裡面。
把下面打日誌的程式碼拷貝到編輯器中:
console.log("hello TypeScript");
按下F5
鍵,你會看到一個像這樣的launch.json
檔案:
{ // 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
檔案,把它改成下面像這樣:
{ // 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
所在的目錄,建立另一個檔案tsconfig.json
。把下面的程式碼拷貝進去:
{ "compilerOptions": { "sourceMap": true } }
這樣,Task Runner
就可以把TypeScript編譯成JavaScript,並且可監聽到檔案的變化,實時編譯。
再次點選Ternimal
標籤,選擇Run Build Task
,再選擇tsc: watch - tsconfig.json
,可以看到終端出現的資訊:
[21:41:31] Starting compilation in watch mode…
你可以使用VS Code的除錯功能編譯TypeScript檔案。
設定好了開發環境,你就可以著手處理TypeScript泛型概念相關的問題了。
找到問題
TypeScript中不建議使用any
型別,原因有幾點,你可以在本文看到。其中一個原因,就是除錯時缺乏完整的資訊。而選擇VS Code作為開發工具的一個很好的理由,就是它帶來的基於這些資訊的智慧感知。
如果你有一個類,儲存著一個集合。有方法向該集合裡新增東西,也有方法通過索引獲取集合裡的東西。像這樣:
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
的話,可以建立一個接受number
而不是string
的集合。著是一個不錯的選擇,但有一個很大的缺點——程式碼重複。程式碼重複,最終會導致編寫和除錯程式碼的時間增多,並且降低記憶體的使用效率。
另一個選擇,是使用any
型別代替string
型別定義剛才的類,像下面這樣:
class Collection { private _things: any[]; constructor() { this._things = []; } add(something: any) { this._things.push(something); } get(index: number): any { return this._things[index]; } }
此時,該集合支援你給出的任何型別。如果你建立像這樣的邏輯構建此集合的話:
let Stringss = new Collection(); Stringss.add("hello"); Stringss.add("world");
這添加了字串"hello"和"world"到集合中,你可以打出像length
這樣的屬性,返回任意一個集合元素的長度。
console.log(Stringss.get(0).length);
字串"hello"有五個字元,執行TypeScript程式碼,你可以在除錯模式下看到它。
請注意,當你滑鼠懸停在length屬性上時,VS Code的智慧感知沒有提供任何資訊,因為它不知道你選擇使用的確切型別。當你像下面這樣,把其中一個新增的元素修改為其他型別時,比如number
,這種不能被智慧感知到的情況會體現得更加明顯:
let Strings = new Collection(); Strings.add(001); Strings.add("world"); console.log(Strings.get(0).length);
你打出一個undefined
的結果,仍然沒有什麼有用資訊。如果你更進一步,決定列印string
的子字串——它會報執行時錯誤,但不指不出任何具體的內容,更重要的是,編譯器沒有給出任何型別不匹配的編譯時錯誤。
console.log(Stringss.get(0).substr(0,1));
這僅僅是使用any
型別定義該集合的一種後果罷了。
理解中心思想
剛才使用any
型別導致的問題,可以用TypeScript中的泛型來解決。其中心思想是型別安全。使用泛型,你可以用一種編譯器能理解的,並且合乎我們判斷的方式,指定類、型別和介面的例項。正如在其他強型別語言中的情況一樣,用這種方法,就可以在編譯時發現你的型別錯誤,從而保證了型別安全。
泛型的語法像這樣:
function identity<T>(arg: T): T { return arg; }
你可以在之前建立的集合中使用泛型,用尖括號括起來。
class Collection<T> { 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<String>(); Stringss.add(001); Stringss.add("world"); console.log(Stringss.get(0).substr(0, 1));
如果將帶有尖括號的新邏輯複製到程式碼編輯器中,你會立即注意到"001"下的波浪線。這是因為,TypeScript現在可以從指定的泛型型別推斷出001不是字串。在T
出現的地方,就可以使用string
型別,這就實現了型別安全。本質上,這個集合的輸出可以是任何型別,但你指明瞭它應該是string
型別,所以編譯器推斷它就是string
型別。這裡使用的泛型宣告是在類級別,它也可以在其他級別定義,如靜態方法級別和例項方法級別,你稍後會看到。
使用泛型
你可以在泛型宣告中,包含多個型別引數,它們只需要用逗號分隔,像這樣:
class Collection<T, K> { private _things: K[]; constructor() { this._things = []; } add(something: K): void { this._things.push(something); } get(index: number): T { console.log(index); } }
宣告時,型別引數也可以在函式中顯式使用,比如:
class Collection { private _things: any[]; constructor() { this._things = []; } add<A>(something: A): void { this._things.push(something); } get<B>(index: number): B { return this._things[index]; } }
因此,當你要建立一個新的集合時,在方法級別宣告的泛型,現在也會在方法呼叫級別中被指示,像這樣:
let Stringss = new Collection(); Stringss.add<string>("hello"); Stringss.add("world");
你還可注意到,在滑鼠懸停時,VS Code智慧感知能夠推斷出第二個add函式呼叫仍然是string
型別。
泛型宣告同樣適用於靜態方法:
static add<A>(something: A): void { _things.push(something); }
雖然初始化靜態方法時,可使用泛型型別,但是,對初始化靜態屬性則不能。
泛型約束
現在,你已經對泛型有比較好的認識,是時候提到泛型的核心缺點及其實用的解決方案了。使用泛型,許多屬性的型別都能被TypeScript推斷出來,然而,在某些TypeScript不能做出準確推斷的地方,它不會做任何假設。為了型別安全,你需要將這些要求或者約束定義為介面,並在泛型初始化中繼承它們。
如果你有這樣一個非常簡單的函式:
function printName<T>(arg: T) { console.log(arg.length); return arg; } printName(3);
因為TypeScript無法推斷出arg
引數是什麼型別,不能證明所有型別都具有length
屬性,因此不能假設它是一個字串(具有length
屬性)。所以,你會在length
屬性下看到一條波浪線。如前所述,你需要建立一個介面,讓泛型的初始化可以繼承它,以便編譯器不再報警。
interface NameArgs { length: number; }
你可以在泛型宣告中繼承它:
function printName<T extends NameArgs>(arg: T) { console.log(arg.length); return arg; }
這告訴TypeScript,可使用任何具有length
屬性的型別。 定義它之後,函式呼叫語句也必須更改,因為它不再適用於所有型別。 所以它應看起來是這樣:
printName({length: 1, value: 3});
這是一個很基礎的例子。但理解了它,你就能看到在使用泛型時,設定泛型約束是多麼有用。
為什麼是泛型
一個活躍於Stack Overflow社群的成員,Behrooz,在後續內容中很好的回答了這個問題。在TypeScript中使用泛型的主要原因是使型別,類或介面充當引數。 它幫助我們為不同型別的輸入重用相同的程式碼,因為型別本身可用作引數。
泛型的一些好處有:
- 定義輸入和輸出引數型別之間的關係。比如
function test<T>(input: T[]): T { //… }
允許你確保輸入和輸出使用相同的型別,儘管輸入是用的陣列。
- 可使用編譯時更強大的型別檢查。在上訴示例中,編譯器讓你知道陣列方法可用於輸入,任何其他方法則不行。
- 你可以去掉不需要的強制型別轉換。比如,如果你有一個常量列表:
Array<Item> a = [];
變數陣列時,你可以由智慧感知訪問到Item型別的所有成員。
其他資源
- 官方文件
結論
你已經看完了泛型概念的概述,並看到了各種示例來幫助揭示它背後的思想。 起初,泛型的概念可能令人困惑,我建議,把本文再讀一遍,並查閱本文所提供的額外資源,幫助自己更好地理解。泛型是一個很棒的概念,可以幫助我們在JavaScript中,更好地控制輸入和輸出。請快樂地編碼