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