JavaScript 中的每個值都有一組行為,您可以通過運行不同的操作來觀察這些行為。這聽起來很抽象,但作為一個簡單的例子,考慮我們可能在名為?message
?的變量上運行的一些操作。
// Accessing the property 'toLowerCase'
// on 'message' and then calling it
message.toLowerCase();// Calling 'message'
message();
如果我們將其分解,第一行可運行的代碼會訪問一個名為?toLowerCase
?的屬性,然后調用它。第二個嘗試直接調用?message
。
但是假設我們不知道?message
?的值——這很常見——我們不能可靠地說明嘗試運行這些代碼會得到什么結果。每個操作的行為完全取決于我們最初擁有的價值。
message
?可以調用嗎?- 它上面是否有一個名為?
toLowerCase
?的屬性? - 如果是這樣,
toLowerCase
?甚至可以調用嗎? - 如果這兩個值都是可調用的,它們會返回什么?
這些問題的答案通常是我們在編寫 JavaScript 時牢記在心的事情,我們必須希望我們得到了正確的所有細節。
假設?message
?是按以下方式定義的。
const message = "Hello World!";
正如您可能猜到的,如果我們嘗試運行?message.toLowerCase()
,我們將只得到相同的小寫字符串。
那第二行代碼呢?如果您熟悉 JavaScript,您會知道這會失敗并出現異常:
TypeError: message is not a function
如果我們能避免這樣的錯誤,那就太好了。
當我們運行我們的代碼時,我們的 JavaScript 運行時選擇做什么的方式是確定值的類型——它具有什么樣的行為和能力。這就是?TypeError
?所暗示的部分內容——它表示字符串?"Hello World!"
?不能作為函數調用。
對于某些值,例如原語?string
?和?number
,我們可以在運行時使用?typeof
?運算符識別它們的類型。但是對于其他的東西,比如函數,沒有相應的運行時機制來識別它們的類型。例如,考慮這個函數:
function fn(x) {return x.flip();
}
我們可以通過閱讀代碼觀察到,這個函數只有在給定一個具有可調用?flip
?屬性的對象時才能工作,但 JavaScript 不會以我們可以在代碼運行時檢查的方式顯示這些信息。在純 JavaScript 中,判斷?fn
?對特定值做了什么的唯一方法是調用它并查看會發生什么。這種行為使得在運行之前很難預測代碼會做什么,這意味著在編寫代碼時更難知道代碼會做什么。
這樣看,類型就是描述哪些值可以傳遞給fn
,哪些會崩潰的概念。JavaScript 僅真正提供動態類型 - 運行代碼以查看發生了什么。
另一種方法是使用靜態類型系統在運行之前預測預期的代碼。
?靜態類型檢查
回想一下我們之前嘗試將?string
?作為函數調用而得到的?TypeError
。大多數人不喜歡在運行他們的代碼時遇到任何類型的錯誤——那些被認為是錯誤!當我們編寫新代碼時,我們會盡力避免引入新的錯誤。
如果我們只添加一點代碼,保存我們的文件,重新運行代碼,然后立即看到錯誤,我們也許可以快速隔離問題;但情況并非總是如此。我們可能沒有對這個功能進行足夠徹底的測試,所以我們可能永遠不會真正遇到可能拋出的潛在錯誤!或者,如果我們有幸目睹了這個錯誤,我們可能最終會進行大規模的重構并添加許多我們不得不挖掘的不同代碼。
理想情況下,我們可以有一個工具來幫助我們在代碼運行之前找到這些錯誤。這就是像 TypeScript 這樣的靜態類型檢查器所做的。靜態類型系統描述了當我們運行程序時我們的值的形狀和行為。像 TypeScript 這樣的類型檢查器使用這些信息并告訴我們什么時候事情可能會出軌。
const message = "hello!";message();
在我們首先運行代碼之前,使用 TypeScript 運行最后一個示例會給我們一個錯誤消息。
?非異常故障
到目前為止,我們一直在討論某些事情,比如運行時錯誤——JavaScript 運行時告訴我們它認為某些事情是荒謬的情況。出現這些情況是因為?ECMAScript 規范?明確說明了語言在遇到意外情況時應該如何表現。
例如,規范說嘗試調用不可調用的東西應該會引發錯誤。也許這聽起來像 "obvious behavior",但您可以想象訪問對象上不存在的屬性也會引發錯誤。相反,JavaScript 為我們提供了不同的行為并返回值?undefined
:
const user = {name: "Daniel",age: 26,
};user.location; // returns undefined
最終,靜態類型系統必須調用其系統中應將哪些代碼標記為錯誤,即使是不會立即拋出錯誤的 "valid" JavaScript。在 TypeScript 中,以下代碼會產生關于?location
?未定義的錯誤:
const user = {name: "Daniel",age: 26,
};user.location;
雖然有時這意味著在您可以表達的內容上進行權衡,但其目的是捕捉我們程序中的合法錯誤。TypeScript 捕獲了很多合法的錯誤。
例如:錯別字,
const announcement = "Hello World!";// How quickly can you spot the typos?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();// We probably meant to write this...
announcement.toLocaleLowerCase();
未調用的函數,
function flipCoin() {// Meant to be Math.random()return Math.random < 0.5;
}
或基本邏輯錯誤。
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {// ...
} else if (value === "b") {// Oops, unreachable
}
?工具類型
當我們在代碼中出錯時,TypeScript 可以捕獲錯誤。這很好,但 TypeScript 也可以從一開始就阻止我們犯這些錯誤。
類型檢查器具有檢查諸如我們是否正在訪問變量和其他屬性的正確屬性之類的信息。一旦有了這些信息,它還可以開始建議您可能想要使用哪些屬性。
這意味著 TypeScript 也可以用于編輯代碼,核心類型檢查器可以在您在編輯器中鍵入時提供錯誤消息和代碼完成。這是人們在談論 TypeScript 工具時經常提到的部分內容。
import express from "express";
const app = express();app.get("/", function (req, res) {res.sen
// ^|
});app.listen(3000);
TypeScript 非常重視工具,這超出了您鍵入時的完成和錯誤。支持 TypeScript 的編輯器可以提供 "quick fixes" 以自動修復錯誤、重構以輕松重新組織代碼,以及用于跳轉到變量定義或查找對給定變量的所有引用的有用導航功能。所有這些都建立在類型檢查器之上,并且是完全跨平臺的,所以很可能是?你最喜歡的編輯器支持 TypeScript。
?tsc,TypeScript 編譯器
我們一直在談論類型檢查,但我們還沒有使用我們的類型檢查器。讓我們熟悉一下我們的新朋友?tsc
,TypeScript 編譯器。首先,我們需要通過 npm 獲取它。
npm install -g typescript
這將全局安裝 TypeScript 編譯器?
tsc
。如果您希望從本地?node_modules
?包運行?tsc
,則可以使用?npx
?或類似工具。
現在讓我們移動到一個空文件夾并嘗試編寫我們的第一個 TypeScript 程序:hello.ts
:
// Greets the world.
console.log("Hello world!");
請注意,這里沒有多余的裝飾;這個 "hello world" 程序看起來和你用 JavaScript 編寫的 "hello world" 程序一樣。現在讓我們通過運行?typescript
?包為我們安裝的命令?tsc
?來檢查它。
tsc hello.ts
Tada!
等等,到底 "tada" 什么?我們跑了?tsc
,什么也沒發生!好吧,沒有類型錯誤,所以我們沒有在控制臺中得到任何輸出,因為沒有什么要報告的。
但再次檢查 - 我們得到了一些文件輸出。如果我們查看當前目錄,我們會在?hello.ts
?旁邊看到一個?hello.js
?文件。這是?tsc
?編譯或轉換為純 JavaScript 文件后我們的?hello.ts
?文件的輸出。如果我們檢查內容,我們會看到 TypeScript 在處理?.ts
?文件后會吐出什么:
// Greets the world.
console.log("Hello world!");
在這種情況下,TypeScript 幾乎不需要轉換,所以它看起來和我們寫的一樣。編譯器試圖發出看起來像人會寫的東西的干凈可讀的代碼。雖然這并不總是那么容易,但 TypeScript 會始終如一地縮進,注意我們的代碼何時跨越不同的代碼行,并試圖保留注釋。
如果我們確實引入了類型檢查錯誤怎么辦?讓我們重寫hello.ts
:
// This is an industrial-grade general-purpose greeter function:
function greet(person, date) {console.log(`Hello ${person}, today is ${date}!`);
}greet("Brendan");
如果我們再次運行?tsc hello.ts
,請注意我們在命令行上收到錯誤!
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
:
tsc --noEmitOnError hello.ts
您會注意到?hello.js
?永遠不會更新。
?顯式的類型
到目前為止,我們還沒有告訴 TypeScript?person
?或?date
?是什么。讓我們編輯代碼來告訴 TypeScript?person
?是一個?string
,而?date
?應該是一個?Date
?對象。我們還將在?date
?上使用?toDateString()
?方法。
function greet(person: string, date: Date) {console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
我們所做的是在?person
?和?date
?上添加類型注釋來描述可以使用哪些類型的值來調用?greet
。您可以將該簽名讀作 "greet
?takes a?person
?of type?string
, and a?date
?of type?Date
"。
有了這個,TypeScript 可以告訴我們?greet
?可能被錯誤調用的其他情況。例如...
function greet(person: string, date: Date) {console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}greet("Maddison", Date());
嗯? TypeScript 在我們的第二個參數上報告了一個錯誤,但是為什么呢?
也許令人驚訝的是,在 JavaScript 中調用?Date()
?返回一個?string
。另一方面,用?new Date()
?構造一個?Date
?實際上給了我們所期望的結果。
無論如何,我們可以快速修復錯誤:
function greet(person: string, date: Date) {console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}greet("Maddison", new Date());
請記住,我們并不總是必須編寫顯式類型注釋。在許多情況下,TypeScript 甚至可以為我們推斷(或 "figure out")類型,即使我們省略它們。
let msg = "hello there!";
即使我們沒有告訴 TypeScript?msg
?有?string
?類型,它也能夠弄清楚這一點。這是一個特性,當類型系統最終會推斷出相同的類型時,最好不要添加注釋。
注意:如果您將鼠標懸停在該單詞上,則上一個代碼示例中的消息氣泡是您的編輯器將顯示的內容。
?擦除的類型
讓我們看看當我們用?tsc
?編譯上面的函數?greet
?以輸出 JavaScript 時會發生什么:
function greet(person: string, date: Date) {console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}greet("Maddison", new Date());
這里注意兩點:
- 我們的?
person
?和?date
?參數不再有類型注釋。 - 我們的 "template string" - 那個使用反引號(
`
?字符)的字符串 - 被轉換為帶有連接的純字符串。
稍后會詳細介紹第二點,但現在讓我們關注第一點。類型注釋不是 JavaScript 的一部分(或者 ECMAScript 是迂腐的),所以實際上沒有任何瀏覽器或其他運行時可以在未經修改的情況下運行 TypeScript。這就是 TypeScript 首先需要一個編譯器的原因——它需要某種方式來剝離或轉換任何 TypeScript 特定的代碼,以便您可以運行它。大多數特定于 TypeScript 的代碼都被刪除了,同樣地,我們的類型注釋也被完全刪除了。
記住:類型注釋永遠不會改變程序的運行時行為。
?降級
與上面的另一個區別是我們的模板字符串是從
`Hello ${person}, today is ${date.toDateString()}!`;
到
"Hello " + person + ", today is " + date.toDateString() + "!";
為什么會這樣?
模板字符串是 ECMAScript 版本中的一項功能,稱為 ECMAScript 2015(又名 ECMAScript 6、ES2015、ES6 等 - 不要問)。TypeScript 能夠將代碼從較新版本的 ECMAScript 重寫為較舊的版本,例如 ECMAScript 3 或 ECMAScript 5(又名 ES3 和 ES5)。從 ECMAScript 的新版本或 "higher" 版本向下移動到舊版本或 "lower" 版本的過程有時稱為降級。
默認情況下,TypeScript 以 ES3 為目標,這是一個非常舊的 ECMAScript 版本。通過使用?target?選項,我們可以選擇更新一點的東西。使用?--target es2015
?運行將 TypeScript 更改為以 ECMAScript 2015 為目標,這意味著代碼應該能夠在任何支持 ECMAScript 2015 的地方運行。所以運行?tsc --target es2015 hello.ts
?會給我們以下輸出:
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
。