JavaScript 中的每個值都有一組您可透過執行不同運算觀察到的行為。這聽起來很抽象,但作為一個快速範例,請考慮我們可能會對名為 message
的變數執行的某些運算。
js
// Accessing the property 'toLowerCase'// on 'message' and then calling itmessage.toLowerCase();// Calling 'message'message();
如果我們將其分解,第一個可執行程式碼行會存取名為 toLowerCase
的屬性,然後呼叫它。第二個會嘗試直接呼叫 message
。
但假設我們不知道 message
的值 - 這是很常見的 - 我們無法可靠地說出我們會從嘗試執行任何這段程式碼中得到什麼結果。每個運算的行為完全取決於我們一開始擁有的值。
message
可呼叫嗎?- 它有一個名為
toLowerCase
的屬性嗎? - 如果有的話,
toLowerCase
甚至可呼叫嗎? - 如果這兩個值都可呼叫,它們會傳回什麼?
當我們撰寫 JavaScript 時,這些問題的答案通常是我們記在腦中的事情,而且我們必須希望我們記對了所有細節。
假設 message
是以以下方式定義的。
js
const message = "Hello World!";
您可能猜到了,如果我們嘗試執行 message.toLowerCase()
,我們會得到完全相同但為小寫的字串。
那第二行程式碼呢?如果您熟悉 JavaScript,您會知道這會失敗並產生例外
txt
TypeError: message is not a function
如果我們可以避免這種錯誤,那會很棒。
當我們執行程式碼時,我們的 JavaScript 執行時間選擇要執行的內容的方式是找出值的類型 - 它有哪些類型的行為和功能。這是 TypeError
所暗示的一部分 - 它表示字串 "Hello World!"
無法作為函式呼叫。
對於某些值,例如基元 string
和 number
,我們可以使用 typeof
運算子在執行時間識別其類型。但對於其他事物,例如函式,沒有對應的執行時間機制來識別其類型。例如,請考慮這個函式
js
function fn(x) {return x.flip();}
我們可以觀察,透過閱讀程式碼,這個函式只會在給予一個具有可呼叫的 flip
屬性的物件時才會運作,但 JavaScript 並不會以我們在程式碼執行時可以檢查的方式顯示這個資訊。在純 JavaScript 中,唯一可以得知 fn
對特定值執行的動作的方法,就是呼叫它並觀察會發生什麼事。這種行為使得在程式碼執行前難以預測程式碼會執行什麼動作,這表示在撰寫程式碼時,更難以得知程式碼會執行什麼動作。
從這個角度來看,類型是描述哪些值可以傳遞給 fn
以及哪些值會導致崩潰的概念。JavaScript 只真正提供動態型別 - 執行程式碼以觀察會發生什麼事。
另一種方法是使用靜態型別系統,在程式碼執行之前預測程式碼預期執行的動作。
靜態型別檢查
回想一下,我們先前嘗試將 字串
呼叫為函式時收到的 TypeError
。大多數人不喜歡在執行程式碼時收到任何類型的錯誤 - 那些被視為錯誤!當我們撰寫新程式碼時,我們會盡力避免引入新的錯誤。
如果我們只新增一點點程式碼,儲存檔案,重新執行程式碼,並立即看到錯誤,我們或許可以快速找出問題所在;但這並非總是如此。我們可能沒有徹底測試功能,因此我們可能永遠不會遇到實際會引發的潛在錯誤!或者,如果我們很幸運地看到錯誤,我們可能最終會進行大規模的重構,並新增大量不同的程式碼,而我們被迫深入探究。
理想情況下,我們可以有一個工具,在我們的程式碼執行之前幫助我們找出這些錯誤。這就是像 TypeScript 這類的靜態類型檢查器所做的。靜態類型系統描述了執行程式時我們數值的外觀和行為。像 TypeScript 這類的類型檢查器會使用該資訊,並在事情可能出軌時告訴我們。
tsTry
constmessage = "hello!";This expression is not callable. Type 'String' has no call signatures.2349This expression is not callable. Type 'String' has no call signatures.(); message
使用 TypeScript 執行最後一個範例,會在我們執行程式碼之前給我們一個錯誤訊息。
非例外失敗
到目前為止,我們一直在討論某些事情,例如執行時期錯誤 - JavaScript 執行時期告訴我們它認為某些事情沒有意義的情況。這些情況出現是因為ECMAScript 規格在遇到意外情況時明確說明語言應如何運作。
例如,規格說明嘗試呼叫不可呼叫的內容應擲回錯誤。這聽起來可能是「明顯的行為」,但你可以想像,存取物件上不存在的屬性也應擲回錯誤。相反,JavaScript 給我們不同的行為並傳回值 undefined
js
const user = {name: "Daniel",age: 26,};user.location; // returns undefined
最終,靜態類型系統必須決定哪些程式碼應在其系統中標示為錯誤,即使它是「有效的」JavaScript,也不會立即擲回錯誤。在 TypeScript 中,下列程式碼會產生關於 location
未定義的錯誤
tsTry
constuser = {name : "Daniel",age : 26,};Property 'location' does not exist on type '{ name: string; age: number; }'.2339Property 'location' does not exist on type '{ name: string; age: number; }'.user .; location
雖然有時這表示在你可以表達的內容中有所取捨,但目的是偵測程式中的合法錯誤。而且 TypeScript 會偵測到很多合法的錯誤。
例如:拼寫錯誤、
tsTry
constannouncement = "Hello World!";// How quickly can you spot the typos?announcement .toLocaleLowercase ();announcement .toLocalLowerCase ();// We probably meant to write this...announcement .toLocaleLowerCase ();
未呼叫函式、
tsTry
functionflipCoin () {// Meant to be Math.random()returnOperator '<' cannot be applied to types '() => number' and 'number'.2365Operator '<' cannot be applied to types '() => number' and 'number'.Math .random < 0.5;}
或基本邏輯錯誤。
tsTry
constvalue =Math .random () < 0.5 ? "a" : "b";if (value !== "a") {// ...} else if (This comparison appears to be unintentional because the types '"a"' and '"b"' have no overlap.2367This comparison appears to be unintentional because the types '"a"' and '"b"' have no overlap.value === "b") {// Oops, unreachable}
工具類型
當我們在程式碼中犯錯時,TypeScript 可以偵測錯誤。這很好,但 TypeScript 也可以防止我們一開始就犯下這些錯誤。
類型檢查器有資訊可以檢查一些事情,例如我們是否存取變數和其他屬性上的正確屬性。一旦它有這些資訊,它也可以開始建議你可能想要使用的屬性。
這表示 TypeScript 可用於編輯程式碼,而且核心型別檢查器可在您於編輯器中輸入時提供錯誤訊息和程式碼完成。這是人們在討論 TypeScript 中的工具時經常提到的部分。
tsTry
importexpress from "express";constapp =express ();app .get ("/", function (req ,res ) {res .sen });app .listen (3000);
TypeScript 認真看待工具,而且不只在您輸入時提供完成和錯誤。支援 TypeScript 的編輯器可提供「快速修正」以自動修正錯誤、重構以輕鬆重新整理程式碼,以及有用的導覽功能以跳至變數定義或尋找特定變數的所有參考。所有這些功能都建置在型別檢查器之上,而且完全跨平台,因此 您最愛的編輯器很可能支援 TypeScript。
tsc
,TypeScript 編譯器
我們一直在討論型別檢查,但我們尚未使用我們的型別檢查器。讓我們認識一下我們的新朋友 tsc
,TypeScript 編譯器。首先,我們需要透過 npm 取得它。
sh
npm install -g typescript
這會在全球安裝 TypeScript 編譯器
tsc
。如果您偏好從本機node_modules
套件執行tsc
,可以使用npx
或類似工具。
現在讓我們移動到一個空資料夾,並嘗試撰寫我們的第一個 TypeScript 程式:hello.ts
tsTry
// Greets the world.console .log ("Hello world!");
請注意,這裡沒有任何裝飾;這個「hello world」程式看起來與您為 JavaScript「hello world」程式所撰寫的程式相同。現在讓我們透過執行 typescript
套件為我們安裝的命令 tsc
來檢查類型。
sh
tsc hello.ts
太棒了!
等等,「太棒了」什麼?我們執行了 tsc
,但什麼事都沒發生!嗯,沒有類型錯誤,因此我們在主控台中沒有得到任何輸出,因為沒有任何需要報告的內容。
但再檢查一次 - 我們反而得到了一些檔案輸出。如果我們查看目前的目錄,我們會在 hello.ts
旁邊看到一個 hello.js
檔案。那是 tsc
將我們的 hello.ts
檔案編譯或轉換成純 JavaScript 檔案後的輸出。如果我們檢查內容,我們會看到 TypeScript 在處理 .ts
檔案後吐出了什麼
js
// Greets the world.console.log("Hello world!");
在這個情況中,TypeScript 幾乎沒有什麼需要轉換的,因此它看起來與我們所撰寫的內容相同。編譯器會嘗試發出乾淨且可讀的程式碼,看起來像是人會撰寫的內容。雖然這並不總是那麼容易,但 TypeScript 會一致縮排,注意我們的程式碼跨越不同程式碼行時,並嘗試保留註解。
如果我們確實引入了類型檢查錯誤,那會怎麼樣?讓我們重新撰寫 hello.ts
tsTry
// This is an industrial-grade general-purpose greeter function:functiongreet (person ,date ) {console .log (`Hello ${person }, today is ${date }!`);}greet ("Brendan");
如果我們再次執行 tsc hello.ts
,請注意我們在命令列上得到一個錯誤!
txt
Expected 2 arguments, but got 1.
TypeScript 告訴我們我們忘記將引數傳遞給 greet
函式,而且這是正確的。到目前為止,我們只撰寫了標準 JavaScript,但類型檢查仍然能夠找出我們程式碼中的問題。感謝 TypeScript!
發出錯誤
你可能沒有從上一個範例中注意到的一件事是我們的 hello.js
檔案又變了。如果我們開啟那個檔案,我們會看到內容基本上看起來和我們的輸入檔案一樣。這可能有點令人驚訝,因為 tsc
回報了關於我們程式碼的錯誤,但這是基於 TypeScript 的核心價值觀之一:很多時候,你比 TypeScript 更了解。
從前面重申,類型檢查程式碼限制了你能夠執行的程式種類,因此類型檢查器發現哪些東西是可以接受的,這之間有一個權衡。大部分時間這沒問題,但有些情況下那些檢查會造成阻礙。例如,想像你自己將 JavaScript 程式碼遷移到 TypeScript 並引進類型檢查錯誤。最後你會清理類型檢查器的事物,但是那個原始的 JavaScript 程式碼已經在運作了!為什麼將它轉換到 TypeScript 會阻止你執行它?
因此 TypeScript 絕不會妨礙您的工作。當然,隨著時間的推移,您可能想要對錯誤採取更為防禦性的措施,並讓 TypeScript 的行為更加嚴格。在這種情況下,您可以使用 noEmitOnError
編譯器選項。嘗試變更您的 hello.ts
檔案,並使用該標記執行 tsc
sh
tsc --noEmitOnError hello.ts
您會注意到 hello.js
永遠不會更新。
明確類型
到目前為止,我們尚未告訴 TypeScript person
或 date
是什麼。讓我們編輯程式碼,告訴 TypeScript person
是 string
,而 date
應該是 Date
物件。我們還將在 date
上使用 toDateString()
方法。
tsTry
functiongreet (person : string,date :Date ) {console .log (`Hello ${person }, today is ${date .toDateString ()}!`);}
我們所做的是在 person
和 date
上新增 類型註解,以描述 greet
可以呼叫哪些類型的值。您可以將該簽章讀為「greet
採用 string
類型的 person
,以及 Date
類型的 date
」。
透過此方式,TypeScript 可以告訴我們 greet
可能在其他情況下被錯誤呼叫。例如…
tsTry
functiongreet (person : string,date :Date ) {console .log (`Hello ${person }, today is ${date .toDateString ()}!`);}Argument of type 'string' is not assignable to parameter of type 'Date'.2345Argument of type 'string' is not assignable to parameter of type 'Date'.greet ("Maddison",Date ());
咦?TypeScript 在我們的第二個引數上回報了一個錯誤,但為什麼呢?
或許令人驚訝的是,在 JavaScript 中呼叫 Date()
會傳回一個 string
。另一方面,使用 new Date()
建構一個 Date
實際上會提供我們預期的結果。
無論如何,我們可以快速修復錯誤
tsTry
functiongreet (person : string,date :Date ) {console .log (`Hello ${person }, today is ${date .toDateString ()}!`);}greet ("Maddison", newDate ());
請記住,我們不一定要寫入明確的類型註解。在許多情況下,即使我們省略了類型註解,TypeScript 甚至可以僅僅推斷(或「找出」)我們的類型。
tsTry
letmsg = "hello there!";
儘管我們沒有告訴 TypeScript msg
具有 string
類型,但它能夠找出這一點。這是一個功能,最好不要在類型系統最終推斷出相同類型時添加註解。
注意:如果您將滑鼠懸停在單字上,前一個程式碼範例中的訊息泡泡就是您的編輯器會顯示的內容。
已清除的類型
讓我們看看當我們使用 tsc
編譯上述函式 greet
以輸出 JavaScript 時會發生什麼事
tsTry
"use strict";function greet(person, date) {console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));}greet("Maddison", new Date());
在此處注意兩件事
- 我們的
person
和date
參數不再有類型註解。 - 我們的「範本字串」- 使用反引號(
`
字元)的那個字串 - 已轉換為帶有連接的純粹字串。
稍後會詳細說明第二個重點,但現在讓我們專注於第一個重點。類型註解不屬於 JavaScript(或嚴格來說的 ECMAScript),因此實際上沒有任何瀏覽器或其他執行時期可以執行未修改的 TypeScript。這就是 TypeScript 首先需要編譯器的原因 - 它需要一些方法來移除或轉換任何特定於 TypeScript 的程式碼,以便您可以執行它。大多數特定於 TypeScript 的程式碼都會被清除,同樣地,這裡我們的類型註解也已完全清除。
請記住:類型註解絕不會改變您的程式執行時期行為。
降級
另一個與上述不同的部分是,我們的範本字串已從
js
`Hello ${person}, today is ${date.toDateString()}!`;
改寫為
js
"Hello ".concat(person, ", today is ").concat(date.toDateString(), "!");
為什麼會這樣?
範本字串是 ECMAScript 稱為 ECMAScript 2015(又稱 ECMAScript 6、ES2015、ES6 等 - 別問)版本中的功能。TypeScript 具有將較新版本的 ECMAScript 重寫為較舊版本(例如 ECMAScript 3 或 ECMAScript 5(又稱 ES3 和 ES5))的能力。從較新或「較高」版本的 ECMAScript 轉換到較舊或「較低」版本的過程有時稱為降級。
預設情況下,TypeScript 以 ES3 為目標,這是 ECMAScript 的極舊版本。我們可以使用 target
選項選擇較新的版本。使用 --target es2015
執行會將 TypeScript 變更為以 ECMAScript 2015 為目標,這表示程式碼應該可以在支援 ECMAScript 2015 的任何地方執行。因此,執行 tsc --target es2015 hello.ts
會產生下列輸出
js
function greet(person, date) {console.log(`Hello ${person}, today is ${date.toDateString()}!`);}greet("Maddison", new Date());
儘管預設目標是 ES3,但絕大多數目前的瀏覽器都支援 ES2015。因此,大多數開發人員都可以安全地指定 ES2015 或以上版本為目標,除非與某些舊瀏覽器的相容性很重要。
嚴格性
不同的使用者會在 TypeScript 中尋找類型檢查器的不同功能。有些人正在尋找一種更寬鬆的選擇加入體驗,這有助於僅驗證程式的一部分,並仍具有良好的工具。這是 TypeScript 的預設體驗,其中類型是選用的,推論採用最寬鬆的類型,並且不會檢查潛在的 null
/undefined
值。很像 tsc
在錯誤時發出訊息,這些預設會設定為不阻礙您。如果您正在遷移現有的 JavaScript,這可能是理想的第一步。
相比之下,許多使用者更喜歡 TypeScript 盡可能直接驗證,這就是該語言也提供嚴格性設定的原因。這些嚴格性設定會將靜態類型檢查從開關(您的程式碼已檢查或未檢查)轉變為更接近刻度盤的東西。您將此刻度盤轉得越高,TypeScript 檢查的內容就越多。這可能需要一些額外的作業,但一般來說,從長遠來看,它會自行支付,並能進行更徹底的檢查和更準確的工具。如果可能,新的程式碼庫應始終開啟這些嚴格性檢查。
TypeScript 有幾個類型檢查嚴格性旗標可以開啟或關閉,除非另有說明,否則我們所有的範例都會在啟用所有旗標的情況下撰寫。CLI 中的 strict
旗標,或 tsconfig.json
中的 "strict": true
會同時開啟所有旗標,但我們可以個別選擇停用。你應該知道最重要的兩個旗標是 noImplicitAny
和 strictNullChecks
。
noImplicitAny
回想一下,在某些地方,TypeScript 沒有嘗試為我們推斷類型,而是退回到最寬鬆的類型:any
。這並非最糟的情況 - 畢竟,退回到 any
只是單純的 JavaScript 體驗。
然而,使用 any
通常會違背一開始使用 TypeScript 的目的。程式碼的類型化程度越高,你將獲得越多的驗證和工具,表示你在編寫程式碼時會遇到較少的錯誤。開啟 noImplicitAny
旗標會對任何類型被隱式推斷為 any
的變數發出錯誤。
strictNullChecks
預設情況下,null
和 undefined
等值可以指定給任何其他類型。這可以讓撰寫某些程式碼變得更容易,但忘記處理 null
和 undefined
是世界上無數錯誤的根源 - 有些人認為這是一個 十億美元的錯誤!strictNullChecks
標記讓處理 null
和 undefined
變得更明確,並免除我們擔心是否忘記處理 null
和 undefined
。