在本章節中,我們將介紹在 JavaScript 程式碼中會找到的一些最常見的類型,並說明在 TypeScript 中描述這些類型的對應方式。這並非詳盡無遺的清單,後續章節將說明更多命名和使用其他類型的途徑。
類型也可以出現在許多其他地方,而不仅仅是類型註解。當我們了解類型本身時,我們還將了解可以在其中參照這些類型以形成新結構的地方。
我們將從檢閱在撰寫 JavaScript 或 TypeScript 程式碼時可能會遇到的最基本和最常見的類型開始。這些類型稍後將形成更複雜類型的核心建構區塊。
基本類型:string
、number
和 boolean
JavaScript 有三個非常常用的基本類型:string
、number
和 boolean
。每個類型在 TypeScript 中都有對應的類型。正如您所預期的那樣,這些名稱與在這些類型的值上使用 JavaScript typeof
算子時所看到的相同
string
代表字串值,例如"Hello, world"
number
則用於數字,例如42
。JavaScript 沒有針對整數的特殊執行時期值,因此沒有等同於int
或float
的類型 - 所有類型都只是number
boolean
則用於兩個值true
和false
類型名稱
String
、Number
和Boolean
(以大寫字母開頭)是合法的,但指的是一些特殊內建類型,在您的程式碼中很少會出現。永遠使用string
、number
或boolean
作為類型。
陣列
若要指定陣列的類型,例如 [1, 2, 3]
,可以使用語法 number[]
;此語法適用於任何類型(例如 string[]
是字串陣列,以此類推)。您也可能會看到寫成 Array<number>
,其意思相同。我們將在介紹泛型時進一步瞭解 T<U>
語法。
請注意,
[number]
是不同的東西;請參閱 元組部分。
any
TypeScript 也有特殊類型 any
,當你不希望特定值導致類型檢查錯誤時,可以使用它。
當值為類型 any
時,你可以存取它的任何屬性(類型也會變成 any
),像呼叫函式一樣呼叫它,將它指定給(或從)任何類型的值,或執行任何在語法上合法的操作。
tsTry
letobj : 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
宣告變數時,你可以選擇性地加入型別註解來明確指定變數的型別
tsTry
letmyName : 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 仍會檢查是否傳遞了正確數量的參數。
傳回類型註解
您也可以加入傳回類型註解。傳回類型註解會出現在參數清單之後
tsTry
functiongetFavoriteNumber (): number {return 26;}
就像變數類型註解一樣,通常不需要傳回類型註解,因為 TypeScript 會根據函數的 return
陳述式推斷函數的傳回類型。上述範例中的類型註解並不會改變任何內容。有些程式碼庫會明確指定傳回類型,以供文件記錄使用、防止意外變更,或只是出於個人偏好。
傳回 Promise 的函式
如果您要註解傳回 Promise 的函式傳回類型,您應該使用 Promise
類型
tsTry
async functiongetFavoriteNumber ():Promise <number> {return 26;}
匿名函式
匿名函式與函式宣告有點不同。當函式出現在 TypeScript 可以判斷其呼叫方式的地方時,該函式的參數會自動賦予類型。
以下是一個範例
tsTry
constnames = ["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
會具有的類型。
這個程序稱為內容類型,因為函式出現的內容會告知其應具有的類型。
類似於推論規則,您不需要明確學習這如何發生,但了解它確實發生有助於您注意到何時不需要類型註解。稍後,我們將看到更多關於值出現的上下文如何影響其類型的範例。
物件類型
除了基本類型之外,您會遇到的最常見的類型是物件類型。這指的是任何具有屬性的 JavaScript 值,幾乎所有值都是如此!要定義物件類型,我們只需列出其屬性和類型即可。
例如,以下是使用類似點的物件的函式
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
。
選用屬性
物件類型也可以指定部分或全部的屬性為選用。要這樣做,請在屬性名稱後加上?
tsTry
functionprintName (obj : {first : string;last ?: string }) {// ...}// Both OKprintName ({first : "Bob" });printName ({first : "Alice",last : "Alisson" });
在 JavaScript 中,如果您存取不存在的屬性,您會取得值undefined
,而不是執行時期錯誤。因此,當您從選用屬性讀取時,您必須在使用前檢查undefined
。
tsTry
functionprintName (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 ());}
聯合類型
TypeScript 的類型系統讓您可以使用各種運算子,根據現有類型建立新的類型。現在我們知道如何撰寫一些類型,是時候開始以有趣的方式組合它們了。
定義聯合類型
您可能會看到的組合類型的第一種方式是聯合類型。聯合類型是由兩個或多個其他類型形成的類型,代表可能是任何一個這些類型的值。我們將這些類型中的每一個稱為聯合的成員。
讓我們撰寫一個可以在字串或數字上運作的函數
tsTry
functionprintId (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 });
使用聯合型別
提供一個與聯合型別相符的值很簡單,只要提供一個與聯合成員任何一個相符的型別即可。如果你有一個聯合型別的值,你該如何使用它?
TypeScript 只會允許一個運算,如果它對聯合的每個成員都是有效的。例如,如果你有一個聯合 string | number
,你不能使用只在 string
上可用的方法
tsTry
functionprintId (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 }
解決方案是使用程式碼縮小聯合,就像你在沒有型別註解的情況下使用 JavaScript 一樣。當 TypeScript 可以根據程式碼結構為一個值推斷出一個更具體的型別時,就會發生縮小。
例如,TypeScript 知道只有一個 string
值會有 typeof
值 "string"
tsTry
functionprintId (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
的函數
tsTry
functionwelcomePeople (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);}
類型聯集看似具有這些類型屬性的交集,這可能會令人困惑。這並非偶然 - 聯集的名稱源自類型理論。聯集
number | string
是透過取用各個類型的值聯集而組成。請注意,給定兩個集合,其中包含關於每個集合的對應事實,只有這些事實的交集適用於集合本身的聯集。例如,如果我們有一個房間裡的人都很高且戴著帽子,而另一個房間裡的人會說西班牙語且戴著帽子,在合併這些房間後,我們唯一知道關於每個人的事情就是他們一定戴著帽子。
類型別名
我們一直透過在類型註解中直接撰寫物件類型和聯集類型來使用它們。這很方便,但通常會想要多次使用相同的類型,並以單一名稱來指稱它。
類型別名正是如此 - 任何類型的名稱。類型別名的語法為
tsTry
typePoint = {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 });
您實際上可以使用類型別名為任何類型命名,而不僅僅是物件類型。例如,類型別名可以命名聯合類型
tsTry
typeID = number | string;
請注意,別名僅為別名 - 您不能使用類型別名來建立相同類型的不同/相異「版本」。當您使用別名時,它就像您寫入別名類型一樣。換句話說,這段程式碼可能看起來是非法的,但根據 TypeScript 是可以的,因為這兩種類型都是相同類型的別名
tsTry
typeUserInputSanitizedString = string;functionsanitizeInput (str : string):UserInputSanitizedString {returnsanitize (str );}// Create a sanitized inputletuserInput =sanitizeInput (getInput ());// Can still be re-assigned with a string thoughuserInput = "new input";
介面
介面宣告是命名物件類型的另一種方式
tsTry
interfacePoint {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 為結構化類型類型系統的原因。
Type Aliases 與 Interfaces 之間的差異
Type aliases 與 interfaces 非常相似,而且在許多情況下,你可以自由選擇它們。interface
的幾乎所有功能都可以在 type
中使用,主要的區別在於,與始終可延伸的 interface 相比,type 無法重新開啟以新增新的屬性。
Interface |
Type |
---|---|
延伸 interface
|
透過交集延伸 type
|
為現有的 interface 新增新的欄位
|
建立 type 之後就無法變更
|
你會在後面的章節中進一步了解這些概念,所以如果你現在還無法理解所有這些概念,請不用擔心。
- 在 TypeScript 版本 4.2 之前,類型別名名稱 可能會出現在錯誤訊息中,有時會取代等效的匿名類型(這可能是或可能不是理想的)。介面將永遠在錯誤訊息中命名。
- 類型別名可能無法參與 宣告合併,但介面可以。
- 介面只能用於 宣告物件的形狀,而不是重新命名基本型別。
- 介面名稱將 永遠以其原始形式出現在錯誤訊息中,但僅在它們被名稱使用時。
在大部分情況下,你可以根據個人喜好進行選擇,而 TypeScript 會告訴你是否需要將其作為另一種宣告類型。如果你想要一個啟發式方法,請使用 interface
,直到你需要使用 type
中的功能。
類型斷言
有時,您會擁有 TypeScript 無法得知的關於值類型的資訊。
例如,如果您使用 document.getElementById
,TypeScript 只知道這會傳回 某種 HTMLElement
,但您可能知道您的網頁會永遠有一個具有給定 ID 的 HTMLCanvasElement
。
在這種情況下,您可以使用 類型斷言 來指定更具體的類型
tsTry
constmyCanvas =document .getElementById ("main_canvas") asHTMLCanvasElement ;
類型斷言就像類型註解,會由編譯器移除,並且不會影響您的程式碼的執行時期行為。
您也可以使用尖括號語法(除非程式碼在 .tsx
檔案中),這兩個語法是等效的
tsTry
constmyCanvas = <HTMLCanvasElement >document .getElementById ("main_canvas");
提醒:因為類型斷言會在編譯時期移除,所以不會有與類型斷言相關聯的執行時期檢查。如果類型斷言錯誤,不會產生例外或
null
。
TypeScript 只允許轉換為 更具體 或 較不具體 版本的類型的類型斷言。此規則可防止「不可能」的強制轉換,例如
tsTry
constConversion 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
,我們稍後會介紹),然後轉換為所需的類型
tsTry
consta =expr as any asT ;
字面值類型
除了通用的類型 string
和 number
,我們可以在類型位置中參考特定的字串和數字。
思考這一點的一種方式是考慮 JavaScript 如何提供不同的方式來宣告變數。var
和 let
都允許變更變數中儲存的內容,而 const
則不允許。這反映在 TypeScript 如何為字面值建立類型的方式中。
tsTry
letchangingString = "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 ;
字面值類型本身並不是很值錢
tsTry
letx : "hello" = "hello";// OKx = "hello";// ...Type '"howdy"' is not assignable to type '"hello"'.2322Type '"howdy"' is not assignable to type '"hello"'.= "howdy"; x
只有一個值的變數沒有什麼用處!
但透過將字面值組合成聯集,你可以表達一個更有用的概念 - 例如,只接受特定已知值集合的函式
tsTry
functionprintText (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" );
數值字面值類型的工作方式相同
tsTry
functioncompare (a : string,b : string): -1 | 0 | 1 {returna ===b ? 0 :a >b ? 1 : -1;}
當然,你可以將這些與非字面值類型結合
tsTry
interfaceOptions {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 會假設該物件的屬性可能會在稍後變更值。例如,如果您撰寫類似以下的程式碼
tsTry
constobj = {counter : 0 };if (someCondition ) {obj .counter = 1;}
TypeScript 不會假設將 1
指定給先前具有 0
的欄位會產生錯誤。另一種說法是,obj.counter
的型別必須是 number
,而不是 0
,因為型別用於決定讀取和寫入行為。
字串也適用相同規則
tsTry
declare 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 會將此程式碼視為有錯誤。
有兩種方法可以解決這個問題。
-
您可以透過在任一位置新增型別斷言來變更推論
ts
Try// 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
將整個物件轉換為字面類型ts
Tryconstreq = {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)的行為。缺乏對這些值的檢查往往是錯誤的主要來源;我們強烈建議人們在實務上可行的情況下開啟 strictNullChecks
。
strictNullChecks
開啟
當 strictNullChecks
開啟 時,如果值為 null
或 undefined
,您需要在對該值使用函式或屬性之前測試這些值。就像在使用選用屬性之前檢查 undefined
一樣,我們可以使用縮小範圍檢查可能為 null
的值
tsTry
functiondoSomething (x : string | null) {if (x === null) {// do nothing} else {console .log ("Hello, " +x .toUpperCase ());}}
非空斷言運算子(後綴 !
)
TypeScript 也有特殊語法,用於從類型中移除 null
和 undefined
,而不進行任何明確檢查。在任何表達式後寫入 !
實際上是一種類型斷言,表示該值不是 null
或 undefined
tsTry
functionliveDangerously (x ?: number | null) {// No errorconsole .log (x !.toFixed ());}
就像其他類型斷言一樣,這不會變更程式碼的執行時期行為,因此只有在您知道值無法為 null
或 undefined
時才使用 !
非常重要。
列舉
列舉是 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()
建立全域唯一參考的基本類型
tsTry
constfirstName =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}
您可以在 符號參考頁面 中了解有關它們的更多資訊。