在本章中,我們將介紹在 JavaScript 程式碼中會遇到的最常見的值型別,並解釋如何在 TypeScript 中描述這些型別。這並不是一個詳盡的列表,後續章節將介紹更多命名和使用其他型別的方法。
型別不僅僅出現在型別註解中,它們還可以出現在許多其他 地方。當我們學習這些型別本身時,也將瞭解可以在哪些地方引用這些型別來構建新的構造。
我們將從回顧在編寫 JavaScript 或 TypeScript 程式碼時可能遇到的最基本、最常見的型別開始。它們稍後將構成更復雜型別的核心構建塊。
基礎型別:string、number 和 boolean
JavaScript 有三種非常常用的 基礎型別:string、number 和 boolean。每種型別在 TypeScript 中都有對應的型別。正如你所料,如果你在這些型別的值上使用 JavaScript 的 typeof 運算子,你也會看到相同的名稱。
string表示字串值,如"Hello, world"number用於數字,如42。JavaScript 沒有專門的整數執行時值,因此沒有int或float的等價物——一切都只是numberboolean用於兩個值true和false
型別名稱
String、Number和Boolean(首字母大寫)是合法的,但它們引用的是一些在程式碼中極少出現的特殊內建型別。始終使用string、number或boolean來定義型別。
陣列
要指定像 [1, 2, 3] 這樣的陣列型別,可以使用 number[] 語法;該語法適用於任何型別(例如 string[] 是字串陣列,依此類推)。你可能還會看到寫成 Array<number> 的形式,它們的含義相同。當我們介紹 泛型 時,會詳細學習 T<U> 語法。
請注意,
[number]是另一種東西;請參閱有關 元組 (Tuples) 的章節。
any
TypeScript 還有一個特殊的型別 any,當你不想讓某個特定的值導致型別檢查錯誤時,可以使用它。
當一個值的型別為 any 時,你可以訪問它的任何屬性(這些屬性的型別也會是 any),像函式一樣呼叫它,將它分配給(或從它分配)任何型別的值,或者進行任何語法上合法的操作。
tsTryletobj : any = {x : 0 };// None of the following lines of code will throw compiler errors.// Using `any` disables all further type checking, and it is assumed// you know the environment better than TypeScript.obj .foo ();obj ();obj .bar = 100;obj = "hello";constn : number =obj ;
當你不想為了讓 TypeScript 認為某行程式碼沒問題而寫出冗長的型別時,any 型別非常有用。
noImplicitAny
當你沒有指定型別,且 TypeScript 無法從上下文推斷出它時,編譯器通常會預設使用 any。
不過,你通常應該避免這種情況,因為 any 不會經過型別檢查。使用編譯器標誌 noImplicitAny 來將任何隱式的 any 標記為錯誤。
變數上的型別註解
當使用 const、var 或 let 宣告變數時,你可以選擇新增型別註解來顯式指定變數的型別。
tsTryletmyName : string = "Alice";
TypeScript 不使用“型別在左側”風格的宣告,例如
int x = 0;。型別註解總是放在被定義內容的 後面。
但在大多數情況下,這不是必需的。儘可能地,TypeScript 會嘗試自動 推斷 程式碼中的型別。例如,變數的型別是根據其初始化的值進行推斷的。
tsTry// No type annotation needed -- 'myName' inferred as type 'string'letmyName = "Alice";
在很大程度上,你不需要顯式學習型別推斷的規則。如果你是初學者,儘量少用型別註解,你可能會驚訝於僅僅需要很少的註解,TypeScript 就能完全理解程式碼的含義。
函式
函式是 JavaScript 中傳遞資料的主要方式。TypeScript 允許你指定函式輸入值和輸出值的型別。
引數型別註解
當你宣告一個函式時,可以在每個引數後面新增型別註解,以宣告函式接收的引數型別。引數型別註解位於引數名稱之後。
tsTry// Parameter type annotationfunctiongreet (name : string) {console .log ("Hello, " +name .toUpperCase () + "!!");}
當引數具有型別註解時,傳遞給該函式的引數將會被檢查。
tsTry// Would be a runtime error if executed!Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.greet (42 );
即使你的引數沒有型別註解,TypeScript 仍然會檢查你是否傳遞了正確數量的引數。
返回型別註解
你也可以新增返回型別註解。返回型別註解出現在引數列表之後。
tsTryfunctiongetFavoriteNumber (): number {return 26;}
與變數型別註解非常相似,你通常不需要返回型別註解,因為 TypeScript 會根據函式的 return 語句推斷出函式的返回型別。上述示例中的型別註解並不會改變任何內容。一些程式碼庫會為了文件目的、防止意外更改或僅出於個人偏好而顯式指定返回型別。
返回 Promise 的函式
如果你想為返回 promise 的函式新增返回型別註解,應該使用 Promise 型別。
tsTryasync functiongetFavoriteNumber ():Promise <number> {return 26;}
匿名函式
匿名函式與函式宣告略有不同。當函數出現在 TypeScript 可以確定其呼叫方式的地方時,該函式的引數會被自動賦予型別。
下面是一個例子:
tsTryconstnames = ["Alice", "Bob", "Eve"];// Contextual typing for function - parameter s inferred to have type stringnames .forEach (function (s ) {console .log (s .toUpperCase ());});// Contextual typing also applies to arrow functionsnames .forEach ((s ) => {console .log (s .toUpperCase ());});
即使引數 s 沒有型別註解,TypeScript 也利用 forEach 函式的型別以及陣列的推斷型別,確定了 s 的型別。
這個過程被稱為 上下文型別推斷 (Contextual Typing),因為函數出現的 上下文 決定了它應該具有的型別。
與推斷規則類似,你不需要明確學習它是如何發生的,但理解它確實會發生,可以幫助你在不需要型別註解時注意到這一點。稍後,我們將看到更多關於值出現的上下文如何影響其型別的示例。
物件型別
除了基礎型別外,你遇到最常見的型別是 物件型別。這指的是任何具有屬性的 JavaScript 值,幾乎所有值都是!要定義物件型別,我們只需列出其屬性及其型別。
例如,這是一個接收類似點物件 (point-like object) 的函式:
tsTry// The parameter's type annotation is an object typefunctionprintCoord (pt : {x : number;y : number }) {console .log ("The coordinate's x value is " +pt .x );console .log ("The coordinate's y value is " +pt .y );}printCoord ({x : 3,y : 7 });
這裡,我們用一個包含兩個屬性(x 和 y,且均為 number 型別)的型別註解了引數。你可以使用 , 或 ; 來分隔屬性,最後一個分隔符無論哪種方式都是可選的。
每個屬性的型別部分也是可選的。如果你沒有指定型別,它將被假定為 any。
可選屬性
物件型別還可以指定某些或所有屬性是 可選的。為此,請在屬性名稱後面新增一個 ?。
tsTryfunctionprintName (obj : {first : string;last ?: string }) {// ...}// Both OKprintName ({first : "Bob" });printName ({first : "Alice",last : "Alisson" });
在 JavaScript 中,如果你訪問一個不存在的屬性,你將得到 undefined 值,而不是執行時錯誤。因此,當你 讀取 一個可選屬性時,在使用它之前必須先檢查 undefined。
tsTryfunctionprintName (obj : {first : string;last ?: string }) {// Error - might crash if 'obj.last' wasn't provided!'obj.last' is possibly 'undefined'.18048'obj.last' is possibly 'undefined'.console .log (obj .last .toUpperCase ());if (obj .last !==undefined ) {// OKconsole .log (obj .last .toUpperCase ());}// A safe alternative using modern JavaScript syntax:console .log (obj .last ?.toUpperCase ());}
聯合型別 (Union Types)
TypeScript 的型別系統允許你使用各種運算子從現有型別構建新型別。既然我們已經學會了如何編寫一些型別,現在是時候開始以有趣的方式 組合 它們了。
定義聯合型別
你可能看到的組合型別的第一種方式是 聯合 型別。聯合型別由兩個或多個其他型別組成,表示可以是這些型別中 任何一個 的值。我們將這些型別稱為聯合的 成員。
讓我們編寫一個可以處理字串或數字的函式:
tsTryfunctionprintId (id : number | string) {console .log ("Your ID is: " +id );}// OKprintId (101);// OKprintId ("202");// ErrorArgument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.2345Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.printId ({myID : 22342 });
允許在第一個元素之前使用聯合成員的分隔符,因此你也可以這樣寫:
tsTryfunctionprintTextOrNumberOrBool (textOrNumberOrBool :| string| number| boolean) {console .log (textOrNumberOrBool );}
處理聯合型別
提供 符合聯合型別的值很容易——只需提供符合聯合成員中任何一個的型別即可。如果你 擁有 一個聯合型別的值,該如何處理它呢?
只有當操作對聯合的 每個 成員都有效時,TypeScript 才會允許該操作。例如,如果你有聯合型別 string | number,則不能使用僅在 string 上可用的方法。
tsTryfunctionprintId (id : number | string) {Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.2339Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.console .log (id .()); toUpperCase }
解決方案是使用程式碼 收窄 (Narrowing) 聯合型別,就像你在沒有型別註解的 JavaScript 中所做的那樣。當 TypeScript 能夠根據程式碼結構推斷出值的更具體的型別時,收窄 就會發生。
例如,TypeScript 知道只有 string 型別的值才會使 typeof 值為 "string"。
tsTryfunctionprintId (id : number | string) {if (typeofid === "string") {// In this branch, id is of type 'string'console .log (id .toUpperCase ());} else {// Here, id is of type 'number'console .log (id );}}
另一個例子是使用像 Array.isArray 這樣的函式:
tsTryfunctionwelcomePeople (x : string[] | string) {if (Array .isArray (x )) {// Here: 'x' is 'string[]'console .log ("Hello, " +x .join (" and "));} else {// Here: 'x' is 'string'console .log ("Welcome lone traveler " +x );}}
請注意,在 else 分支中,我們不需要做任何特殊的事情——如果 x 不是 string[],那麼它一定是一個 string。
有時你擁有的聯合型別中,所有成員都有共同點。例如,陣列和字串都有 slice 方法。如果聯合中的每個成員都有一個共同的屬性,你可以直接使用該屬性,而無需收窄。
tsTry// Return type is inferred as number[] | stringfunctiongetFirstThree (x : number[] | string) {returnx .slice (0, 3);}
聯合型別似乎擁有這些型別屬性的 交集,這可能會令人困惑。這並非偶然——聯合 (union) 這個名稱來源於型別理論。聯合
number | string是透過獲取每個型別 值的並集 來組成的。請注意,給定兩個關於集合的對應事實,只有這些事實的 交集 才適用於集合本身的 聯合。例如,如果我們有一個房間裡站著戴帽子的高個子,另一個房間裡站著戴帽子的西班牙語使用者,合併這兩個房間後,我們對 每個人 唯一確定的一點是:他們一定戴著帽子。
類型別名 (Type Aliases)
我們一直透過直接在型別註解中編寫物件型別和聯合型別來使用它們。這很方便,但通常我們希望多次使用同一個型別,並用一個單一的名稱來引用它。
類型別名 正是如此——它是任何 型別 的 名稱。類型別名的語法是:
tsTrytypePoint = {x : number;y : number;};// Exactly the same as the earlier examplefunctionprintCoord (pt :Point ) {console .log ("The coordinate's x value is " +pt .x );console .log ("The coordinate's y value is " +pt .y );}printCoord ({x : 100,y : 100 });
你實際上可以使用類型別名為任何型別命名,而不僅僅是物件型別。例如,類型別名可以命名一個聯合型別:
tsTrytypeID = number | string;
請注意,別名 只是 別名——你不能使用類型別名來建立同一個型別的不同/獨特的“版本”。當你使用別名時,效果就如同你直接編寫了被別名的型別一樣。換句話說,這段程式碼可能 看起來 非法,但根據 TypeScript 的規則它是合法的,因為這兩個型別都是同一個型別的別名。
tsTrytypeUserInputSanitizedString = string;functionsanitizeInput (str : string):UserInputSanitizedString {returnsanitize (str );}// Create a sanitized inputletuserInput =sanitizeInput (getInput ());// Can still be re-assigned with a string thoughuserInput = "new input";
介面 (Interfaces)
介面宣告 是命名物件型別的另一種方式:
tsTryinterfacePoint {x : number;y : number;}functionprintCoord (pt :Point ) {console .log ("The coordinate's x value is " +pt .x );console .log ("The coordinate's y value is " +pt .y );}printCoord ({x : 100,y : 100 });
就像上面使用類型別名一樣,這個示例的效果與使用匿名物件型別完全相同。TypeScript 只關心傳遞給 printCoord 的值的 結構——它只關心該值是否具有預期的屬性。只關注型別的結構和能力,這就是為什麼我們將 TypeScript 稱為 結構化型別 系統。
類型別名與介面的區別
類型別名和介面非常相似,在許多情況下你可以自由選擇。interface 的幾乎所有功能在 type 中都可用,主要的區別在於型別無法重新開啟以新增新屬性,而介面始終是可擴充套件的。
介面: |
型別 |
|---|---|
|
擴充套件介面:
|
透過交集擴充套件型別:
|
|
向現有介面新增新欄位:
|
型別建立後無法更改:
|
你將在後面的章節中學習更多關於這些概念的內容,所以如果現在不能完全理解也沒關係。
- 在 TypeScript 4.2 版本之前,類型別名名稱 可能 出現在錯誤資訊中,有時會替代等效的匿名型別(這可能是也可能不是期望的)。介面總是會在錯誤資訊中顯示名稱。
- 類型別名可能無法參與 宣告合併,但介面可以。
- 介面只能用於 宣告物件的形狀,而不能重新命名基礎型別。
- 介面名稱在錯誤資訊中 總是 以其原始形式出現,但 僅 在它們按名稱使用時。
- 與交集型別相比,在編譯器中將介面與
extends一起使用 通常能獲得更好的效能。
在大多數情況下,你可以根據個人喜好進行選擇,如果 TypeScript 需要某種特定型別的宣告,它會告訴你。如果需要一個經驗法則,請使用 interface,直到你需要使用 type 的特有功能時再進行切換。
型別斷言 (Type Assertions)
有時你擁有 TypeScript 無法獲知的值型別資訊。
例如,如果你正在使用 document.getElementById,TypeScript 只能知道它返回 某種 HTMLElement,但你可能知道你的頁面總是會有一個給定 ID 的 HTMLCanvasElement。
在這種情況下,你可以使用 型別斷言 來指定更具體的型別:
tsTryconstmyCanvas =document .getElementById ("main_canvas") asHTMLCanvasElement ;
像型別註解一樣,型別斷言會被編譯器移除,不會影響程式碼的執行時行為。
你也可以使用尖括號語法(除非程式碼是在 .tsx 檔案中),它們是等價的:
tsTryconstmyCanvas = <HTMLCanvasElement >document .getElementById ("main_canvas");
提醒:因為型別斷言在編譯時被移除,所以型別斷言不涉及執行時檢查。如果型別斷言錯誤,不會丟擲異常或產生
null。
TypeScript 只允許轉換為 更具體 或 不太具體 的版本的型別斷言。此規則防止了“不可能的”強制轉換,例如:
tsTryconstConversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.2352Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.x = "hello" as number;
有時這條規則可能過於保守,會禁止可能有效的更復雜的強制轉換。如果發生這種情況,你可以使用兩次斷言:先斷言為 any(或 unknown,我們稍後會介紹),然後再斷言為目標型別。
tsTryconsta =expr as any asT ;
字面量型別 (Literal Types)
除了通用的 string 和 number 型別外,我們還可以在型別位置引用 具體的 字串和數字。
考慮這一點的一種方式是看 JavaScript 宣告變數的不同方式。var 和 let 允許改變變數中儲存的內容,而 const 則不允許。這反映在 TypeScript 如何為字面量建立型別上。
tsTryletchangingString = "Hello World";changingString = "Olá Mundo";// Because `changingString` can represent any possible string, that// is how TypeScript describes it in the type systemchangingString ;constconstantString = "Hello World";// Because `constantString` can only represent 1 possible string, it// has a literal type representationconstantString ;
單獨來看,字面量型別價值不大:
tsTryletx : "hello" = "hello";// OKx = "hello";// ...Type '"howdy"' is not assignable to type '"hello"'.2322Type '"howdy"' is not assignable to type '"hello"'.= "howdy"; x
只能有一個值的變數沒什麼用!
但透過將字面量 組合 成聯合型別,你可以表達一個更有用的概念——例如,僅接受一組特定已知值的函式:
tsTryfunctionprintText (s : string,alignment : "left" | "right" | "center") {// ...}printText ("Hello, world", "left");Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.2345Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.printText ("G'day, mate","centre" );
數字字面量型別的工作方式相同:
tsTryfunctioncompare (a : string,b : string): -1 | 0 | 1 {returna ===b ? 0 :a >b ? 1 : -1;}
當然,你可以將它們與非字面量型別組合使用:
tsTryinterfaceOptions {width : number;}functionconfigure (x :Options | "auto") {// ...}configure ({width : 100 });configure ("auto");Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.2345Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.configure ("automatic" );
還有另一種字面量型別:布林字面量。只有兩種布林字面量型別,正如你可能猜到的,它們是 true 和 false。boolean 型別本身實際上只是聯合型別 true | false 的別名。
字面量推斷
當你用物件初始化變數時,TypeScript 會假設該物件的屬性以後可能會改變值。例如,如果你寫下這樣的程式碼:
tsTryconstobj = {counter : 0 };if (someCondition ) {obj .counter = 1;}
TypeScript 不會認為將 1 分配給之前儲存 0 的欄位是錯誤的。換句話說,obj.counter 的型別必須是 number,而不是 0,因為型別被用來確定 讀取 和 寫入 行為。
這同樣適用於字串:
tsTrydeclare functionhandleRequest (url : string,method : "GET" | "POST"): void;constreq = {url : "https://example.com",method : "GET" };Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.2345Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.handleRequest (req .url ,req .method );
在上面的例子中,req.method 被推斷為 string,而不是 "GET"。因為程式碼可以在建立 req 和呼叫 handleRequest 之間被執行,且可能會分配一個新的字串(如 "GUESS")給 req.method,所以 TypeScript 認為這段程式碼有錯誤。
有兩種解決方法。
-
你可以透過在任意位置新增型別斷言來更改推斷:
tsTry// Change 1:constreq = {url : "https://example.com",method : "GET" as "GET" };// Change 2handleRequest (req .url ,req .method as "GET");修改 1 的意思是“我打算讓
req.method始終具有 字面量型別"GET"”,防止之後將"GUESS"分配給該欄位。修改 2 的意思是“因為其他原因,我知道req.method的值是"GET"”。 -
你可以使用
as const將整個物件轉換為字面量型別:tsTryconstreq = {url : "https://example.com",method : "GET" } asconst ;handleRequest (req .url ,req .method );
as const 字尾的作用類似於 const,但它是針對型別系統的,確保所有屬性都被賦予字面量型別,而不是 string 或 number 這種更通用的型別。
null 和 undefined
JavaScript 有兩個用於表示缺失或未初始化值的原始值:null 和 undefined。
TypeScript 有兩個同名的對應 型別。這些型別的行為方式取決於你是否開啟了 strictNullChecks 選項。
strictNullChecks 關閉
在 strictNullChecks 關閉 的情況下,可能為 null 或 undefined 的值仍然可以被正常訪問,並且 null 和 undefined 值可以分配給任何型別的屬性。這類似於沒有空檢查的語言(如 C#、Java)的行為。缺乏對這些值的檢查往往是 bug 的主要來源;我們始終建議人們在程式碼庫中開啟 strictNullChecks,如果實際可行的話。
strictNullChecks 開啟
在 strictNullChecks 開啟 的情況下,當一個值為 null 或 undefined 時,你需要在使用該值的方法或屬性之前先進行測試。就像在使用可選屬性之前檢查 undefined 一樣,我們可以使用 收窄 來檢查可能為 null 的值。
tsTryfunctiondoSomething (x : string | null) {if (x === null) {// do nothing} else {console .log ("Hello, " +x .toUpperCase ());}}
非空斷言運算子(字尾 !)
TypeScript 還有一種特殊的語法,用於在不進行任何顯式檢查的情況下從型別中移除 null 和 undefined。在任何表示式後面寫上 !,實際上就是一種型別斷言,即該值不為 null 或 undefined。
tsTryfunctionliveDangerously (x ?: number | null) {// No errorconsole .log (x !.toFixed ());}
與其他型別斷言一樣,這不會改變程式碼的執行時行為,因此僅在你確定該值 不可能是 null 或 undefined 時使用 ! 是很重要的。
列舉 (Enums)
列舉是 TypeScript 新增到 JavaScript 的功能,它允許描述一個值,該值可以是一組可能的命名常量之一。與大多數 TypeScript 功能不同,這不是新增到 JavaScript 的型別級新增,而是新增到語言和執行時中的功能。因此,這是一個你應該知道它存在,但除非確定需要,否則最好暫時不要使用的功能。你可以在 列舉參考頁面 閱讀更多關於列舉的內容。
不太常見的基礎型別
值得一提的是 JavaScript 中剩餘的在型別系統中表示的基礎型別,儘管我們不會在這裡深入探討。
bigint
從 ES2020 開始,JavaScript 中有一個用於超大整數的基礎型別 BigInt。
tsTry// Creating a bigint via the BigInt functionconstoneHundred : bigint =BigInt (100);// Creating a BigInt via the literal syntaxconstanotherHundred : bigint = 100n;
你可以在 TypeScript 3.2 發行說明 中瞭解更多關於 BigInt 的資訊。
symbol
JavaScript 中有一個用於透過函式 Symbol() 建立全域性唯一引用的基礎型別。
tsTryconstfirstName =Symbol ("name");constsecondName =Symbol ("name");if (This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.2367This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.firstName ===secondName ) {// Can't ever happen}
你可以在 Symbol 參考頁面 中瞭解更多資訊。