基礎知識

歡迎閱讀手冊的第一頁。如果你是第一次接觸 TypeScript,建議先從“入門指南”開始閱讀。

JavaScript 中的每一個值都有其行為特徵,你可以透過執行不同的操作來觀察這些行為。這聽起來可能比較抽象,舉個簡單的例子,考慮我們對一個名為 message 的變數執行的某些操作。

js
// Accessing the property 'toLowerCase'
// on 'message' and then calling it
message.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!" 不能作為函式被呼叫。

對於某些值,例如基本型別 stringnumber,我們可以在執行時使用 typeof 運算子識別它們的型別。但對於函式等其他事物,沒有相應的執行時機制來識別它們的型別。例如,考慮這個函式:

js
function fn(x) {
return x.flip();
}

我們可以透過閱讀程式碼觀察到,只有當傳入一個具有可呼叫 flip 屬性的物件時,該函式才能正常工作。但 JavaScript 並沒有以一種可以在程式碼執行時進行檢查的方式來公開這些資訊。在純 JavaScript 中,確定 fn 對特定值做什麼的唯一方法就是呼叫它看看會發生什麼。這種行為使得在執行程式碼之前很難預測它會做什麼,這意味著在你編寫程式碼時,更難預知程式碼的行為。

從這個角度來看,型別是一種描述哪些值可以傳遞給 fn 以及哪些值會導致程式崩潰的概念。JavaScript 實際上只提供動態型別——即透過執行程式碼來檢視結果。

另一種選擇是使用靜態型別系統,在程式碼執行之前對其預期行為進行預測。

靜態型別檢查

回想一下我們之前嘗試將 string 當作函式呼叫時遇到的 TypeError大多數人都不喜歡在執行程式碼時遇到任何形式的錯誤——那些被認為是 Bug!而當我們編寫新程式碼時,我們總是盡力避免引入新的 Bug。

如果我們新增一點點程式碼,儲存檔案,重新執行,然後立即看到錯誤,我們或許能快速隔離問題;但情況並不總是這樣。我們可能沒有充分測試該功能,因此可能永遠不會真正遇到那個潛在的錯誤!或者如果我們幸運地發現了錯誤,可能已經進行了大規模重構並添加了許多不同的程式碼,不得不深入挖掘。

理想情況下,我們能有一個工具來幫助我們在程式碼執行之前發現這些 Bug。這就是像 TypeScript 這樣的靜態型別檢查器所做的事情。靜態型別系統描述了程式執行時值的形態和行為。像 TypeScript 這樣的型別檢查器會利用這些資訊,並在事情可能出錯時告訴我們。

ts
const message = "hello!";
 
message();
This expression is not callable. Type 'String' has no call signatures.2349This expression is not callable. Type 'String' has no call signatures.
Try

用 TypeScript 執行上一個示例,在執行程式碼之前就會給我們一個錯誤訊息。

非異常失敗

到目前為止,我們一直在討論諸如執行時錯誤之類的事情——即 JavaScript 執行時認為某些操作毫無意義的情況。這些情況的出現是因為 ECMAScript 規範 對語言在遇到意外情況時應如何表現有明確的說明。

例如,規範說明嘗試呼叫不可呼叫的物件應該丟擲錯誤。也許這聽起來是“顯而易見的行為”,但你可以想象訪問物件上不存在的屬性也應該丟擲錯誤。相反,JavaScript 給出了不同的行為並返回了 undefined

js
const user = {
name: "Daniel",
age: 26,
};
user.location; // returns undefined

最終,靜態型別系統必須決定哪些程式碼應被標記為錯誤,即使它是不會立即丟擲錯誤的“合法” JavaScript 程式碼。在 TypeScript 中,以下程式碼會產生關於 location 未定義的錯誤:

ts
const user = {
name: "Daniel",
age: 26,
};
 
user.location;
Property 'location' does not exist on type '{ name: string; age: number; }'.2339Property 'location' does not exist on type '{ name: string; age: number; }'.
Try

雖然這有時意味著在表達能力上做出了一些權衡,但其目的是為了捕獲程式中合法的 Bug。而且 TypeScript 確實捕獲了大量合法的 Bug。

例如:拼寫錯誤、

ts
const announcement = "Hello World!";
 
// How quickly can you spot the typos?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();
 
// We probably meant to write this...
announcement.toLocaleLowerCase();
Try

未呼叫的函式、

ts
function flipCoin() {
// Meant to be Math.random()
return Math.random < 0.5;
Operator '<' cannot be applied to types '() => number' and 'number'.2365Operator '<' cannot be applied to types '() => number' and 'number'.
}
Try

或基本的邏輯錯誤。

ts
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
// ...
} else if (value === "b") {
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.
// Oops, unreachable
}
Try

工具化的型別支援

TypeScript 可以在我們犯錯時捕獲 Bug。這很棒,但 TypeScript 也能防止我們在第一時間犯下這些錯誤。

型別檢查器擁有檢查諸如“我們是否在變數上訪問了正確的屬性”等資訊。一旦擁有這些資訊,它還可以開始建議你可能想要使用的屬性。

這意味著 TypeScript 也可以用來輔助程式碼編輯,核心型別檢查器可以在編輯器中輸入時提供錯誤資訊和程式碼補全。這就是人們在談論 TypeScript 中的工具支援時所指的一部分。

ts
import express from "express";
const app = express();
 
app.get("/", function (req, res) {
res.sen
         
});
 
app.listen(3000);
Try

TypeScript 非常重視工具支援,這遠不止於輸入時的補全和報錯。支援 TypeScript 的編輯器可以提供“快速修復”來自動修正錯誤,透過重構輕鬆重新組織程式碼,以及用於跳轉到變數定義或查詢給定變數所有引用的有用導航功能。這一切都建立在型別檢查器的基礎之上,並且完全跨平臺,因此你喜愛的編輯器很可能已經提供了 TypeScript 支援

tsc,TypeScript 編譯器

我們一直在談論型別檢查,但還沒有真正使用我們的型別檢查器。讓我們來認識一下我們的新朋友 tsc——TypeScript 編譯器。首先,我們需要透過 npm 獲取它。

sh
npm install -g typescript

這會全域性安裝 TypeScript 編譯器 tsc。如果你更喜歡從本地 node_modules 包執行 tsc,可以使用 npx 或類似的工具。

現在讓我們切換到一個空資料夾,嘗試編寫我們的第一個 TypeScript 程式:hello.ts

ts
// Greets the world.
console.log("Hello world!");
Try

注意這裡沒有任何花哨的東西;這個“hello world”程式看起來和你為 JavaScript 編寫的“hello world”程式完全一樣。現在,讓我們透過執行由 typescript 包為我們安裝的 tsc 命令來檢查它的型別。

sh
tsc hello.ts

噹噹!

等等,“噹噹”什麼?我們運行了 tsc 但什麼也沒發生!好吧,因為沒有任何型別錯誤,所以控制檯沒有輸出任何內容,因為沒什麼可報告的。

但再檢查一下——我們得到了一些檔案輸出。如果我們檢視當前目錄,會看到 hello.ts 旁邊多了一個 hello.js 檔案。這就是 tschello.ts 編譯轉換為純 JavaScript 檔案後的輸出。如果我們檢視內容,會看到 TypeScript 處理 .ts 檔案後匯出的程式碼。

js
// Greets the world.
console.log("Hello world!");

在這種情況下,TypeScript 幾乎沒有什麼需要轉換的,所以它看起來和我們寫的一模一樣。編譯器會嘗試輸出乾淨且可讀的程式碼,看起來就像人類編寫的一樣。雖然這並不總是那麼容易,但 TypeScript 會保持縮排一致,注意程式碼跨行的情況,並儘量保留註釋。

如果我們確實引入了一個型別檢查錯誤呢?讓我們重寫 hello.ts

ts
// This is an industrial-grade general-purpose greeter function:
function greet(person, date) {
console.log(`Hello ${person}, today is ${date}!`);
}
 
greet("Brendan");
Try

如果我們再次執行 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 persondate 是什麼。讓我們編輯程式碼,告訴 TypeScript person 是一個 string,而 date 應該是一個 Date 物件。我們還將使用 date 上的 toDateString() 方法。

ts
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
Try

我們所做的是在 persondate 上添加了型別註解,以描述 greet 可以使用什麼型別的值來呼叫。你可以這樣閱讀該簽名:“greet 接受一個型別為 stringperson,以及一個型別為 Datedate”。

有了這個,TypeScript 就可以告訴我們 greet 可能被錯誤呼叫的其他情況。例如……

ts
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", Date());
Argument of type 'string' is not assignable to parameter of type 'Date'.2345Argument of type 'string' is not assignable to parameter of type 'Date'.
Try

嗯?TypeScript 報告了關於我們第二個引數的錯誤,為什麼?

也許令人驚訝,在 JavaScript 中呼叫 Date() 會返回一個 string。而另一方面,使用 new Date() 構造 Date 實際上給出了我們所期望的內容。

無論如何,我們可以快速修復這個錯誤:

ts
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
 
greet("Maddison", new Date());
Try

請記住,我們並不總是需要編寫顯式的型別註解。在許多情況下,即使我們省略它們,TypeScript 甚至可以直接推斷(或“計算出”)型別。

ts
let msg = "hello there!";
let msg: string
Try

即使我們沒有告訴 TypeScript msg 的型別是 string,它也能推斷出來。這是一個特性,如果型別系統最終能推斷出相同的型別,最好不要添加註解。

注意:上一段程式碼示例中的氣泡資訊是當你滑鼠懸停在該詞上時,編輯器會顯示的內容。

型別擦除

讓我們看看當我們將上面的 greet 函式用 tsc 編譯輸出 JavaScript 時會發生什麼。

ts
"use strict";
function greet(person, date) {
console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
greet("Maddison", new Date());
 
Try

這裡注意兩件事:

  1. 我們的 persondate 引數不再有型別註解。
  2. 我們的“模板字串”——即使用反引號(` 字元)的字串——被轉換成了帶有連線符的普通字串。

稍後會詳細介紹第二點,但現在我們先專注於第一點。型別註解不是 JavaScript 的一部分(嚴格來說不是 ECMAScript 的一部分),所以確實沒有任何瀏覽器或其他執行時可以直接執行未經修改的 TypeScript。這就是為什麼 TypeScript 首先需要編譯器的原因——它需要某種方式來剝離或轉換任何 TypeScript 特有的程式碼,以便你可以執行它。大多數 TypeScript 特有的程式碼會被擦除,同樣地,這裡的型別註解被完全擦除了。

記住:型別註解永遠不會改變程式的執行時行為。

降級編譯 (Downleveling)

與上面相比的另一個區別是我們的模板字串被重寫了:

js
`Hello ${person}, today is ${date.toDateString()}!`;

變成了:

js
"Hello ".concat(person, ", today is ").concat(date.toDateString(), "!");

為什麼會這樣?

模板字串是 ECMAScript 2015(又名 ECMAScript 6, ES2015, ES6 等——別問)版本中的一項特性。TypeScript 有能力將較新版本的 ECMAScript 程式碼重寫為較舊的版本,如 ECMAScript 3 或 ECMAScript 5(又名 ES5)。這種從較新或“更高階”的 ECMAScript 版本轉換到較舊或“更低階”版本的過程有時被稱為降級編譯 (downleveling)

預設情況下,TypeScript 的目標版本是 ES5,這是一個非常古老的 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());

雖然預設目標是 ES5,但絕大多數現代瀏覽器都支援 ES2015。因此,除非需要相容某些古老的瀏覽器,否則大多數開發者可以放心地指定 ES2015 或更高版本作為目標。

嚴格性

不同的使用者使用 TypeScript 是出於不同的目的。有些人正在尋找一種更寬鬆的“選擇性加入”體驗,這有助於驗證程式的一部分,同時仍然擁有不錯的工具支援。這是 TypeScript 的預設體驗,其中型別是可選的,推斷會使用最寬鬆的型別,並且不會檢查潛在的 null/undefined 值。正如 tsc 在出現錯誤時依然生成輸出一樣,這些預設設定是為了不妨礙你。如果你正在遷移現有的 JavaScript,這可能是一個理想的起步。

相比之下,許多使用者更喜歡讓 TypeScript 從一開始就儘可能多地進行驗證,這就是為什麼語言也提供了嚴格性設定。這些嚴格性設定將靜態型別檢查從一個開關(要麼開啟,要麼關閉)變成了一個接近於“刻度盤”的東西。你調得越高,TypeScript 為你做的檢查就越多。這可能需要一點額外的工作,但通常來說長遠來看是非常值得的,並且可以實現更徹底的檢查和更準確的工具支援。如果可能的話,新的程式碼庫應該始終開啟這些嚴格性檢查。

TypeScript 有幾個可以開啟或關閉的型別檢查嚴格性標誌,除非另有說明,否則我們的所有示例都將開啟它們。CLI 中的 strict 標誌,或 tsconfig.json 中的 "strict": true 會同時開啟它們,但我們也可以分別停用它們。你應該瞭解的兩個最重要的標誌是 noImplicitAnystrictNullChecks

noImplicitAny

回想一下,在某些地方,TypeScript 不會嘗試為我們推斷型別,而是回退到最寬鬆的型別:any。這不是最糟糕的情況——畢竟,回退到 any 本來就是普通的 JavaScript 體驗。

然而,使用 any 往往違背了使用 TypeScript 的初衷。你的程式型別越明確,你得到的驗證和工具支援就越多,這意味著你在編碼時遇到的 Bug 就會越少。開啟 noImplicitAny 標誌將對任何隱式推斷為 any 的變數發出錯誤警告。

strictNullChecks

預設情況下,nullundefined 等值可以賦值給任何其他型別。這使得編寫某些程式碼變得更容易,但忘記處理 nullundefined 是世界上無數 Bug 的根源——有些人將其視為“十億美元的錯誤”!strictNullChecks 標誌使得處理 nullundefined 更加明確,並省去了我們需要擔心是否忘記處理它們的問題。

TypeScript 文件是一個開源專案。歡迎提交 Pull Request 來幫助我們改進這些頁面 ❤

此頁面的貢獻者
RCRyan Cavanaugh (55)
OTOrta Therox (13)
RTRich Trott (4)
DRDaniel Rosenwasser (4)
EBEli Barzilay (2)
19+

最後更新:2026 年 3 月 27 日