假設我們有一個名為 padLeft
的函式。
tsTry
functionpadLeft (padding : number | string,input : string): string {throw newError ("Not implemented yet!");}
如果 padding
是 number
,它會將其視為我們要加到 input
前面的空格數。如果 padding
是 string
,它應該只將 padding
加到 input
前面。讓我們嘗試實作當 padLeft
傳遞 number
給 padding
時的邏輯。
tsTry
functionpadLeft (padding : number | string,input : string): string {return " ".Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.2345Argument of type 'string | number' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.repeat () + padding input ;}
糟糕,我們在 padding
上收到錯誤。TypeScript 警告我們,我們傳遞了類型為 number | string
的值給 repeat
函式,而它只接受 number
,而且這是正確的。換句話說,我們沒有先明確檢查 padding
是否為 number
,也沒有處理它是 string
的情況,所以我們來做這件事。
tsTry
functionpadLeft (padding : number | string,input : string): string {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
如果這看起來大多像是無趣的 JavaScript 程式碼,那正是重點所在。除了我們放入的註解之外,這個 TypeScript 程式碼看起來就像 JavaScript。其概念是 TypeScript 的型別系統旨在讓撰寫典型的 JavaScript 程式碼盡可能容易,而不必為了取得型別安全性而費盡心思。
雖然看起來可能沒什麼,但實際上這裡發生了很多事。就像 TypeScript 使用靜態型別分析執行時期值一樣,它會在 JavaScript 的執行時期控制流程建構上覆蓋型別分析,例如 if/else
、條件三元運算子、迴圈、真值檢查等,這些都可能影響這些型別。
在我們的 if
檢查中,TypeScript 會看到 typeof padding === "number"
,並將其視為一種稱為型別防護的特殊程式碼形式。TypeScript 會遵循我們的程式可以執行的可能執行路徑,以分析在給定位置的值最具體的可能型別。它會查看這些特殊檢查(稱為型別防護)和指定,而將型別精煉為比宣告更具體的型別的程序稱為縮小。在許多編輯器中,我們可以觀察這些型別的變化,我們甚至會在範例中這麼做。
tsTry
functionpadLeft (padding : number | string,input : string): string {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
TypeScript 了解縮小的幾種不同建構。
typeof
型別防護
正如我們所見,JavaScript 支援一個 typeof
營運子,它可以在執行階段提供關於我們值的型別的非常基本的資訊。TypeScript 預期它會傳回一組特定的字串
"字串"
"數字"
"大整數"
"布林值"
"符號"
"未定義"
"物件"
"函式"
就像我們在 padLeft
中所看到的,這個營運子在許多 JavaScript 函式庫中出現的頻率相當高,而 TypeScript 可以了解它以縮小不同分支中的型別。
在 TypeScript 中,針對 typeof
傳回的值進行檢查是一個型別防護。由於 TypeScript 編碼了 typeof
在不同值上運作的方式,因此它知道 JavaScript 中的一些怪癖。例如,請注意在上面的清單中,typeof
沒有傳回字串 null
。檢視以下範例
tsTry
functionprintAll (strs : string | string[] | null) {if (typeofstrs === "object") {for (const'strs' is possibly 'null'.18047'strs' is possibly 'null'.s of) { strs console .log (s );}} else if (typeofstrs === "string") {console .log (strs );} else {// do nothing}}
在 printAll
函式中,我們嘗試檢查 strs
是否為物件,以查看它是否為陣列型別(現在可能是加強陣列在 JavaScript 中為物件型別的好時機)。但事實證明,在 JavaScript 中,typeof null
實際上是 "object"
!這是歷史上不幸的意外之一。
有足夠經驗的使用者可能不會感到驚訝,但並非所有人都會在 JavaScript 中遇到這種情況;幸運的是,TypeScript 讓我們知道 strs
僅縮小為 string[] | null
,而不是僅 string[]
。
這可能是我們將稱為「真值性」檢查的一個好的切入點。
真值性縮小
「真值性」可能不是你會在字典中找到的字,但這絕對是你會在 JavaScript 中聽到的東西。
在 JavaScript 中,我們可以在條件式、&&
、||
、if
陳述式、布林運算取反 (!
) 中使用任何表達式,還有更多。舉例來說,if
陳述式並不要求其條件永遠是 boolean
類型。
tsTry
functiongetUsersOnlineMessage (numUsersOnline : number) {if (numUsersOnline ) {return `There are ${numUsersOnline } online now!`;}return "Nobody's here. :(";}
在 JavaScript 中,像 if
這樣的建構式會先將其條件「強制轉換」為 boolean
以便理解它們,然後根據結果是 true
或 false
來選擇其分支。像以下這樣的數值
0
NaN
""
(空字串)0n
(零的bigint
版本)null
undefined
全部強制轉換為 false
,而其他數值則強制轉換為 true
。你總是可以用 Boolean
函式或使用較短的雙重布林運算取反來將數值強制轉換為 boolean
。(後者的好處是 TypeScript 會推斷出一個狹義的文字布林類型 true
,而將前者推斷為類型 boolean
。)
tsTry
// both of these result in 'true'Boolean ("hello"); // type: boolean, value: true!!"world"; // type: true, value: true
利用此行為相當普遍,特別是為了防範像 null
或 undefined
這樣的數值。舉例來說,讓我們嘗試對我們的 printAll
函式使用它。
tsTry
functionprintAll (strs : string | string[] | null) {if (strs && typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}
你會注意到,我們透過檢查 strs
是否為真值性來消除上述錯誤。這至少可以防止我們在像以下這樣執行我們的程式碼時出現可怕的錯誤
txt
TypeError: null is not iterable
不過請記住,對基本類型進行真值性檢查通常容易出錯。舉例來說,考慮使用不同的方式來撰寫 printAll
tsTry
functionprintAll (strs : string | string[] | null) {// !!!!!!!!!!!!!!!!// DON'T DO THIS!// KEEP READING// !!!!!!!!!!!!!!!!if (strs ) {if (typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}}
我們將函式的整個主體包在一個真值性檢查中,但這有一個微妙的缺點:我們可能無法再正確處理空字串的情況。
TypeScript 完全不會傷害我們,但如果你不太熟悉 JavaScript,則值得注意此行為。TypeScript 通常可以幫助你及早發現錯誤,但如果你選擇對一個數值「什麼都不做」,它能做的就只有這麼多了,而不會過於強制。如果你願意,你可以使用 linter 確保處理像這樣的狀況。
關於以真值收窄的最後一點說明是,使用 !
的布林運算會從否定的分支中濾出。
tsTry
functionmultiplyAll (values : number[] | undefined,factor : number): number[] | undefined {if (!values ) {returnvalues ;} else {returnvalues .map ((x ) =>x *factor );}}
相等收窄
TypeScript 也會使用 switch
陳述式和相等檢查,例如 ===
、!==
、==
和 !=
來收窄類型。例如
tsTry
functionexample (x : string | number,y : string | boolean) {if (x ===y ) {// We can now call any 'string' method on 'x' or 'y'.x .toUpperCase ();y .toLowerCase ();} else {console .log (x );console .log (y );}}
當我們在上述範例中檢查 x
和 y
是否相等時,TypeScript 知道它們的類型也必須相等。由於 字串
是 x
和 y
都可以採用的唯一共用類型,因此 TypeScript 知道 x
和 y
在第一個分支中必定是 字串
。
檢查特定字面值(相對於變數)也適用。在我們關於真值收窄的章節中,我們寫了一個 printAll
函式,由於它意外地沒有正確處理空字串,因此容易出錯。我們可以執行特定檢查來封鎖 null
,而 TypeScript 仍然會正確地從 strs
的類型中移除 null
。
tsTry
functionprintAll (strs : string | string[] | null) {if (strs !== null) {if (typeofstrs === "object") {for (consts ofstrs ) {console .log (s );}} else if (typeofstrs === "string") {console .log (strs );}}}
JavaScript 中使用 ==
和 !=
進行較寬鬆的相等性檢查也會正確地縮小範圍。如果您不熟悉,檢查某個東西 == null
實際上不僅檢查它是否特別是值 null
- 它還檢查它是否可能是 undefined
。同樣適用於 == undefined
:它檢查一個值是否為 null
或 undefined
。
tsTry
interfaceContainer {value : number | null | undefined;}functionmultiplyValue (container :Container ,factor : number) {// Remove both 'null' and 'undefined' from the type.if (container .value != null) {console .log (container .value );// Now we can safely multiply 'container.value'.container .value *=factor ;}}
in
運算子縮小範圍
JavaScript 有個運算子用於判斷物件或其原型鏈是否有一個具有名稱的屬性:in
運算子。TypeScript 將此視為縮小潛在類型的一種方式。
例如,使用以下程式碼:"value" in x
。其中 "value"
是字串文字,而 x
是聯合類型。「真」分支縮小 x
的類型,這些類型具有可選或必需的屬性 value
,而「假」分支縮小到具有可選或遺失屬性 value
的類型。
tsTry
typeFish = {swim : () => void };typeBird = {fly : () => void };functionmove (animal :Fish |Bird ) {if ("swim" inanimal ) {returnanimal .swim ();}returnanimal .fly ();}
重申一下,可選屬性將存在於縮小的兩側。例如,人類既可以游泳,又可以飛(使用適當的設備),因此應該出現在 in
檢查的兩側
tsTry
typeFish = {swim : () => void };typeBird = {fly : () => void };typeHuman = {swim ?: () => void;fly ?: () => void };functionmove (animal :Fish |Bird |Human ) {if ("swim" inanimal ) {animal ;} else {animal ;}}
instanceof
narrowing
JavaScript 有個用於檢查值是否為另一個值的「實例」的運算子。更具體地說,在 JavaScript 中,x instanceof Foo
會檢查 x
的原型鏈是否包含 Foo.prototype
。雖然我們在此不會深入探討,而且當我們進入類別時,你會看到更多,但它們仍然可用於大多數可以使用 new
建構的值。正如你可能猜到的,instanceof
也是一個類型守衛,而 TypeScript 會在受 instanceof
守衛的分支中縮小範圍。
tsTry
functionlogValue (x :Date | string) {if (x instanceofDate ) {console .log (x .toUTCString ());} else {console .log (x .toUpperCase ());}}
指派
正如我們先前提到的,當我們指派給任何變數時,TypeScript 會查看指派的右側,並適當地縮小左側的範圍。
tsTry
letx =Math .random () < 0.5 ? 10 : "hello world!";x = 1;console .log (x );x = "goodbye!";console .log (x );
請注意,這些指定都為有效。即使在我們第一次指定後,x
的觀察類型已變更為 number
,我們仍然可以將 string
指定給 x
。這是因為 x
的宣告類型(x
開始的類型)為 string | number
,而可指定性總是會針對宣告類型進行檢查。
如果我們將 boolean
指定給 x
,我們會看到一個錯誤,因為這不是宣告類型的一部分。
tsTry
letx =Math .random () < 0.5 ? 10 : "hello world!";x = 1;console .log (x );Type 'boolean' is not assignable to type 'string | number'.2322Type 'boolean' is not assignable to type 'string | number'.= true; x console .log (x );
控制流程分析
到目前為止,我們已經瀏覽了一些 TypeScript 在特定分支中如何縮小的基本範例。但除了從每個變數往上走並在 if
、while
、條件式等中尋找類型防護之外,還有一些事情正在進行中。例如
tsTry
functionpadLeft (padding : number | string,input : string) {if (typeofpadding === "number") {return " ".repeat (padding ) +input ;}returnpadding +input ;}
padLeft
從其第一個 if
區塊中傳回。TypeScript 能夠分析此程式碼,並看到在 padding
為 number
的情況下,其餘主體(return padding + input;
)是無法到達的。因此,它能夠從 padding
的類型中移除 number
(從 string | number
縮小為 string
),以供函式使用。
這種基於可及性的程式碼分析稱為控制流程分析,而 TypeScript 會使用此流程分析來縮小類型,因為它會遇到類型防護和指派。分析變數時,控制流程可能會不斷地分開和重新合併,而該變數在每個點都可能被觀察到具有不同的類型。
tsTry
functionexample () {letx : string | number | boolean;x =Math .random () < 0.5;console .log (x );if (Math .random () < 0.5) {x = "hello";console .log (x );} else {x = 100;console .log (x );}returnx ;}
使用類型謂詞
到目前為止,我們已經使用現有的 JavaScript 結構來處理縮小,但是有時您希望更直接地控制整個程式碼中類型的變化方式。
若要定義使用者定義的類型防護,我們只需定義一個其回傳類型為類型謂詞的函式
tsTry
functionisFish (pet :Fish |Bird ):pet isFish {return (pet asFish ).swim !==undefined ;}
pet is Fish
是此範例中的類型謂詞。謂詞採用 parameterName is Type
的形式,其中 parameterName
必須是目前函式簽章中參數的名稱。
任何時候都使用 isFish
呼叫某些變數,如果原始類型相容,TypeScript 會將該變數縮小到該特定類型。
tsTry
// Both calls to 'swim' and 'fly' are now okay.letpet =getSmallPet ();if (isFish (pet )) {pet .swim ();} else {pet .fly ();}
請注意,TypeScript 不僅知道 pet
在 if
分支中是 Fish
;它還知道在 else
分支中,您沒有 Fish
,所以您必須有 Bird
。
您可以使用類型防護 isFish
來篩選 Fish | Bird
陣列並取得 Fish
陣列
tsTry
constzoo : (Fish |Bird )[] = [getSmallPet (),getSmallPet (),getSmallPet ()];constunderWater1 :Fish [] =zoo .filter (isFish );// or, equivalentlyconstunderWater2 :Fish [] =zoo .filter (isFish ) asFish [];// The predicate may need repeating for more complex examplesconstunderWater3 :Fish [] =zoo .filter ((pet ):pet isFish => {if (pet .name === "sharkey") return false;returnisFish (pet );});
此外,類別可以使用 this is Type
來縮小其類型。
斷言函式
類型也可以使用 斷言函式 來縮小。
辨別聯合
到目前為止,我們看過的大多數範例都集中在使用簡單類型(例如 string
、boolean
和 number
)縮小單一變數。雖然這很常見,但我們在 JavaScript 中大部分時間會處理稍微更複雜的結構。
為了激發動機,讓我們想像我們正在嘗試編碼形狀,例如圓形和正方形。圓形追蹤其半徑,而正方形追蹤其邊長。我們將使用一個名為 kind
的欄位來告訴我們正在處理哪個形狀。以下是定義 Shape
的第一次嘗試。
tsTry
interfaceShape {kind : "circle" | "square";radius ?: number;sideLength ?: number;}
請注意,我們使用字串文字類型的聯集:"circle"
和 "square"
來告訴我們是否應分別將形狀視為圓形或正方形。透過使用 "circle" | "square"
代替 string
,我們可以避免拼寫錯誤問題。
tsTry
functionhandleShape (shape :Shape ) {// oops!if (This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.2367This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.shape .kind === "rect") {// ...}}
我們可以撰寫一個 getArea
函式,根據函式是否處理圓形或正方形來套用正確的邏輯。我們將先嘗試處理圓形。
tsTry
functiongetArea (shape :Shape ) {return'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.Math .PI *shape .radius ** 2;}
在 strictNullChecks
下,這會給我們一個錯誤,這是適當的,因為 radius
可能未定義。但如果我們對 kind
屬性執行適當的檢查,會如何?
tsTry
functiongetArea (shape :Shape ) {if (shape .kind === "circle") {return'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.Math .PI *shape .radius ** 2;}}
嗯,TypeScript 仍然不知道這裡該怎麼做。我們已經達到一個我們比類型檢查器更了解我們的值的點。我們可以嘗試使用非空斷言(shape.radius
後面的 !
)來說明 radius
絕對存在。
tsTry
functiongetArea (shape :Shape ) {if (shape .kind === "circle") {returnMath .PI *shape .radius ! ** 2;}}
但這感覺不太理想。我們必須對類型檢查器大聲喊叫,使用那些非空斷言 (!
) 來說服它 shape.radius
已定義,但如果我們開始移動程式碼,這些斷言容易出錯。此外,在 strictNullChecks
之外,我們無論如何都可以意外存取其中任何欄位(因為在讀取時,假設選用屬性始終存在)。我們絕對可以做得更好。
使用這種編碼 Shape
的問題在於,類型檢查器無法根據 kind
屬性知道 radius
或 sideLength
是否存在。我們需要將我們知道的事情傳達給類型檢查器。有鑑於此,讓我們再試一次定義 Shape
。
tsTry
interfaceCircle {kind : "circle";radius : number;}interfaceSquare {kind : "square";sideLength : number;}typeShape =Circle |Square ;
在這裡,我們已適當地將 Shape
分離為兩個類型,kind
屬性具有不同的值,但 radius
和 sideLength
在各自的類型中宣告為必填屬性。
當我們嘗試存取 Shape
的 radius
時,讓我們看看這裡會發生什麼事。
tsTry
functiongetArea (shape :Shape ) {returnProperty 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.2339Property 'radius' does not exist on type 'Shape'. Property 'radius' does not exist on type 'Square'.Math .PI *shape .** 2; radius }
與我們對 Shape
的第一個定義一樣,這仍然是一個錯誤。當 radius
為選用時,我們會收到一個錯誤(已啟用 strictNullChecks
),因為 TypeScript 無法判斷該屬性是否存在。現在 Shape
是一個聯合,TypeScript 告訴我們 shape
可能是一個 Square
,而 Square
上沒有定義 radius
!兩種解釋都是正確的,但只有 Shape
的聯合編碼會導致錯誤,而不管 strictNullChecks
如何設定。
但是,如果我們再次嘗試檢查 kind
屬性呢?
tsTry
functiongetArea (shape :Shape ) {if (shape .kind === "circle") {returnMath .PI *shape .radius ** 2;}}
這樣可以消除錯誤!當聯合中的每個類型都包含具有文字類型的共用屬性時,TypeScript 會將其視為辨別聯合,並可以縮小聯合的成員。
在此情況下,kind
是共用屬性(這就是 Shape
的辨別屬性)。檢查 kind
屬性是否為 "circle"
會消除 Shape
中沒有類型為 "circle"
的 kind
屬性的所有類型。這將 shape
縮小到類型 Circle
。
相同的檢查也適用於 switch
陳述式。現在,我們可以嘗試撰寫完整的 getArea
,而不會有任何令人討厭的 !
非空斷言。
tsTry
functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;}}
這裡的重要事項是 Shape
的編碼。向 TypeScript 傳達正確的資訊 - Circle
和 Square
實際上是兩個具有特定 kind
欄位的不同類型 - 至關重要。這樣做讓我們可以撰寫類型安全的 TypeScript 程式碼,其看起來與我們原本會撰寫的 JavaScript 沒有什麼不同。從那裡,類型系統能夠執行「正確」的事情,並找出 switch
陳述式中每個分支中的類型。
順便一提,試著玩玩上面的範例,並移除一些 return 關鍵字。你會發現,在意外地落入 switch 陳述式中的不同子句時,類型檢查可以幫助避免錯誤。
辨別聯合不僅可用於討論圓形和正方形。它們很適合用於表示 JavaScript 中的任何訊息傳遞機制,例如透過網路傳送訊息(用戶端/伺服器通訊),或編碼狀態管理架構中的變異。
never
類型
在縮小時,你可以將聯合選項減少到移除所有可能性且一無所有的程度。在這種情況下,TypeScript 將使用 never
類型來表示不應存在的狀態。
窮盡性檢查
never
類型可指定給每個類型;但是,沒有類型可指定給 never
(除了 never
本身)。這表示你可以使用縮小並依賴 never
出現以在 switch
陳述式中進行窮盡性檢查。
例如,在我們的 getArea
函式中新增一個 default
,嘗試將形狀指定給 never
,在處理完所有可能的情況時,不會產生錯誤。
tsTry
typeShape =Circle |Square ;functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;default:const_exhaustiveCheck : never =shape ;return_exhaustiveCheck ;}}
在 Shape
聯合中新增一個新成員,將導致 TypeScript 錯誤
tsTry
interfaceTriangle {kind : "triangle";sideLength : number;}typeShape =Circle |Square |Triangle ;functiongetArea (shape :Shape ) {switch (shape .kind ) {case "circle":returnMath .PI *shape .radius ** 2;case "square":returnshape .sideLength ** 2;default:constType 'Triangle' is not assignable to type 'never'.2322Type 'Triangle' is not assignable to type 'never'.: never = _exhaustiveCheck shape ;return_exhaustiveCheck ;}}