對於習慣使用其他靜態型別語言(如 C# 和 Java)的程式設計師來說,TypeScript 是一個受歡迎的選擇。
TypeScript 的型別系統提供了許多相同的好處,例如更好的程式碼補全、更早的錯誤檢測以及程式各部分之間更清晰的交流。雖然 TypeScript 為這些開發人員提供了許多熟悉的功能,但值得退一步看看 JavaScript(因此也就是 TypeScript)與傳統 OOP 語言有何不同。瞭解這些差異將幫助您編寫更好的 JavaScript 程式碼,並避免直接從 C#/Java 轉到 TypeScript 的程式設計師可能遇到的常見陷阱。
共同學習 JavaScript
如果您已經熟悉 JavaScript,但主要是 Java 或 C# 程式設計師,那麼此介紹頁面可以幫助您解釋一些您可能容易產生的常見誤解和陷阱。TypeScript 對型別進行建模的某些方式與 Java 或 C# 有很大不同,在學習 TypeScript 時記住這一點很重要。
如果您是剛接觸 JavaScript 的 Java 或 C# 程式設計師,我們建議您先學習一點不帶型別的 JavaScript,以瞭解 JavaScript 的執行時行為。因為 TypeScript 不會改變程式碼的執行方式,所以您仍然必須瞭解 JavaScript 的工作原理,才能編寫出真正有用的程式碼!
記住這一點很重要:TypeScript 使用與 JavaScript 相同的執行時,因此任何關於如何實現特定執行時行為(將字串轉換為數字、顯示警報、將檔案寫入磁碟等)的資源都同樣適用於 TypeScript 程式。不要將自己侷限於 TypeScript 特定的資源!
重新思考類
C# 和 Java 是我們可以稱之為強制 OOP 的語言。在這些語言中,類是程式碼組織的基本單位,也是執行時所有資料和行為的基本容器。強制將所有功能和資料儲存在類中對於某些問題來說可能是一個很好的領域模型,但並非每個領域都需要以這種方式表示。
自由函式和資料
在 JavaScript 中,函式可以存在於任何地方,資料也可以自由傳遞,而無需處於預定義的 class 或 struct 中。這種靈活性非常強大。“自由”函式(那些不與類關聯的函式)在沒有隱含 OOP 層次結構的情況下處理資料,往往是編寫 JavaScript 程式的首選模型。
靜態類
此外,C# 和 Java 中的某些構造(如單例和靜態類)在 TypeScript 中是不必要的。
TypeScript 中的 OOP
儘管如此,如果您願意,仍然可以使用類!有些問題非常適合透過傳統的 OOP 層次結構來解決,TypeScript 對 JavaScript 類的支援將使這些模型更加強大。TypeScript 支援許多常見的模式,例如實現介面、繼承和靜態方法。
我們將在本指南的後面介紹類。
重新思考型別
TypeScript 對型別的理解實際上與 C# 或 Java 有很大不同。讓我們探討一些差異。
名義式具體化型別系統 (Nominal Reified Type Systems)
在 C# 或 Java 中,任何給定的值或物件都有一個確切的型別——要麼是 null,要麼是基元,要麼是已知的類型別。我們可以呼叫像 value.GetType() 或 value.getClass() 這樣的方法來在執行時查詢確切型別。此型別的定義將存在於某個具有名稱的類中,除非存在顯式的繼承關係或共同實現的介面,否則我們不能使用兩個形狀相似的類來相互替換。
這些方面描述了一個具體化、名義式的型別系統。我們在程式碼中編寫的型別在執行時是存在的,並且型別是透過它們的宣告而不是結構來關聯的。
作為集合的型別
在 C# 或 Java 中,認為執行時型別與其編譯時宣告之間存在一一對應關係是有意義的。
在 TypeScript 中,最好將型別視為共享某些共同點的數值集合。因為型別只是集合,所以一個特定的值可以同時屬於多個集合。
一旦開始將型別視為集合,某些操作就會變得非常自然。例如,在 C# 中,傳遞既是 string 又是 int 的值會很尷尬,因為沒有單一的型別可以表示這種值。
在 TypeScript 中,一旦意識到每個型別只是一個集合,這就會變得非常自然。如何描述一個既屬於 string 集合又屬於 number 集合的值?它只需屬於這些集合的並集:string | number。
TypeScript 提供了多種以集合論方式處理型別的機制,如果您將型別視為集合,會發現它們更直觀。
擦除式結構型別 (Erased Structural Types)
在 TypeScript 中,物件不是單一的特定型別。例如,如果我們構建了一個滿足介面的物件,即使兩者之間沒有宣告性關係,我們也可以在需要該介面的地方使用該物件。
tsTryinterfacePointlike {x : number;y : number;}interfaceNamed {name : string;}functionlogPoint (point :Pointlike ) {console .log ("x = " +point .x + ", y = " +point .y );}functionlogName (x :Named ) {console .log ("Hello, " +x .name );}constobj = {x : 0,y : 0,name : "Origin",};logPoint (obj );logName (obj );
TypeScript 的型別系統是結構化的,而不是名義式的:我們可以將 obj 用作 Pointlike,因為它具有 x 和 y 屬性,並且它們都是數字。型別之間的關係由它們包含的屬性決定,而不是由它們是否以某種特定的關係宣告決定。
TypeScript 的型別系統也不是具體化的:執行時沒有任何東西會告訴我們 obj 是 Pointlike。事實上,Pointlike 型別在執行時沒有任何形式的存在。
回到作為集合的型別的想法,我們可以認為 obj 既是 Pointlike 數值集合的成員,也是 Named 數值集合的成員。
結構化型別的後果
OOP 程式設計師經常對結構化型別的兩個特定方面感到驚訝。
空型別
首先是空型別似乎違背了預期。
tsTryclassEmpty {}functionfn (arg :Empty ) {// do something?}// No error, but this isn't an 'Empty' ?fn ({k : 10 });
TypeScript 透過檢視提供的引數是否是有效的 Empty 來確定這裡對 fn 的呼叫是否有效。它透過檢查 { k: 10 } 和 class Empty { } 的結構來做到這一點。我們可以看到 { k: 10 } 擁有 Empty 的所有屬性,因為 Empty 沒有屬性。因此,這是一個有效的呼叫!
這看起來可能令人驚訝,但這最終與名義式 OOP 語言中強制執行的關係非常相似。子類不能刪除其基類的屬性,因為這樣做會破壞派生類與其基類之間的自然子型別關係。結構化型別系統只是透過根據擁有相容型別的屬性來描述子型別,從而隱含地識別這種關係。
相同的型別
另一個常見的驚奇來源是相同的型別。
tsclass Car {drive() {// hit the gas}}class Golfer {drive() {// hit the ball far}}// No error?let w: Car = new Golfer();
同樣,這不是錯誤,因為這些類的結構是相同的。雖然這看起來是一個潛在的困惑來源,但在實踐中,不應該相關的相同類並不常見。
我們將在“類”一章中瞭解更多關於類如何相互關聯的內容。
反射
OOP 程式設計師習慣於能夠查詢任何值的型別,即使是泛型型別。
csharp// C#static void LogType<T>() {Console.WriteLine(typeof(T).Name);}
由於 TypeScript 的型別系統是完全擦除的,因此關於泛型型別引數例項化等資訊在執行時不可用。
JavaScript 確實有一些有限的基元,如 typeof 和 instanceof,但請記住,這些運算子仍然作用於型別擦除後的輸出程式碼中存在的值。例如,typeof (new Car()) 將是 "object",而不是 Car 或 "Car"。
後續步驟
這是對日常 TypeScript 中使用的語法和工具的簡要概述。從這裡開始,您可以
- 閱讀完整手冊 從頭到尾
- 探索 Playground 示例