縮小

假設我們有一個名為 padLeft 的函式。

ts
function padLeft(padding: number | string, input: string): string {
throw new Error("Not implemented yet!");
}
Try

如果 paddingnumber,它會將其視為我們要加到 input 前面的空格數。如果 paddingstring,它應該只將 padding 加到 input 前面。讓我們嘗試實作當 padLeft 傳遞 numberpadding 時的邏輯。

ts
function padLeft(padding: number | string, input: string): string {
return " ".repeat(padding) + input;
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'.
}
Try

糟糕,我們在 padding 上收到錯誤。TypeScript 警告我們,我們傳遞了類型為 number | string 的值給 repeat 函式,而它只接受 number,而且這是正確的。換句話說,我們沒有先明確檢查 padding 是否為 number,也沒有處理它是 string 的情況,所以我們來做這件事。

ts
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Try

如果這看起來大多像是無趣的 JavaScript 程式碼,那正是重點所在。除了我們放入的註解之外,這個 TypeScript 程式碼看起來就像 JavaScript。其概念是 TypeScript 的型別系統旨在讓撰寫典型的 JavaScript 程式碼盡可能容易,而不必為了取得型別安全性而費盡心思。

雖然看起來可能沒什麼,但實際上這裡發生了很多事。就像 TypeScript 使用靜態型別分析執行時期值一樣,它會在 JavaScript 的執行時期控制流程建構上覆蓋型別分析,例如 if/else、條件三元運算子、迴圈、真值檢查等,這些都可能影響這些型別。

在我們的 if 檢查中,TypeScript 會看到 typeof padding === "number",並將其視為一種稱為型別防護的特殊程式碼形式。TypeScript 會遵循我們的程式可以執行的可能執行路徑,以分析在給定位置的值最具體的可能型別。它會查看這些特殊檢查(稱為型別防護)和指定,而將型別精煉為比宣告更具體的型別的程序稱為縮小。在許多編輯器中,我們可以觀察這些型別的變化,我們甚至會在範例中這麼做。

ts
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
(parameter) padding: number
}
return padding + input;
(parameter) padding: string
}
Try

TypeScript 了解縮小的幾種不同建構。

typeof 型別防護

正如我們所見,JavaScript 支援一個 typeof 營運子,它可以在執行階段提供關於我們值的型別的非常基本的資訊。TypeScript 預期它會傳回一組特定的字串

  • "字串"
  • "數字"
  • "大整數"
  • "布林值"
  • "符號"
  • "未定義"
  • "物件"
  • "函式"

就像我們在 padLeft 中所看到的,這個營運子在許多 JavaScript 函式庫中出現的頻率相當高,而 TypeScript 可以了解它以縮小不同分支中的型別。

在 TypeScript 中,針對 typeof 傳回的值進行檢查是一個型別防護。由於 TypeScript 編碼了 typeof 在不同值上運作的方式,因此它知道 JavaScript 中的一些怪癖。例如,請注意在上面的清單中,typeof 沒有傳回字串 null。檢視以下範例

ts
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
'strs' is possibly 'null'.18047'strs' is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
Try

printAll 函式中,我們嘗試檢查 strs 是否為物件,以查看它是否為陣列型別(現在可能是加強陣列在 JavaScript 中為物件型別的好時機)。但事實證明,在 JavaScript 中,typeof null 實際上是 "object"!這是歷史上不幸的意外之一。

有足夠經驗的使用者可能不會感到驚訝,但並非所有人都會在 JavaScript 中遇到這種情況;幸運的是,TypeScript 讓我們知道 strs 僅縮小為 string[] | null,而不是僅 string[]

這可能是我們將稱為「真值性」檢查的一個好的切入點。

真值性縮小

「真值性」可能不是你會在字典中找到的字,但這絕對是你會在 JavaScript 中聽到的東西。

在 JavaScript 中,我們可以在條件式、&&||if 陳述式、布林運算取反 (!) 中使用任何表達式,還有更多。舉例來說,if 陳述式並不要求其條件永遠是 boolean 類型。

ts
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
Try

在 JavaScript 中,像 if 這樣的建構式會先將其條件「強制轉換」為 boolean 以便理解它們,然後根據結果是 truefalse 來選擇其分支。像以下這樣的數值

  • 0
  • NaN
  • ""(空字串)
  • 0n(零的 bigint 版本)
  • null
  • undefined

全部強制轉換為 false,而其他數值則強制轉換為 true。你總是可以用 Boolean 函式或使用較短的雙重布林運算取反來將數值強制轉換為 boolean。(後者的好處是 TypeScript 會推斷出一個狹義的文字布林類型 true,而將前者推斷為類型 boolean。)

ts
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
Try

利用此行為相當普遍,特別是為了防範像 nullundefined 這樣的數值。舉例來說,讓我們嘗試對我們的 printAll 函式使用它。

ts
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
Try

你會注意到,我們透過檢查 strs 是否為真值性來消除上述錯誤。這至少可以防止我們在像以下這樣執行我們的程式碼時出現可怕的錯誤

txt
TypeError: null is not iterable

不過請記住,對基本類型進行真值性檢查通常容易出錯。舉例來說,考慮使用不同的方式來撰寫 printAll

ts
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// DON'T DO THIS!
// KEEP READING
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
Try

我們將函式的整個主體包在一個真值性檢查中,但這有一個微妙的缺點:我們可能無法再正確處理空字串的情況。

TypeScript 完全不會傷害我們,但如果你不太熟悉 JavaScript,則值得注意此行為。TypeScript 通常可以幫助你及早發現錯誤,但如果你選擇對一個數值「什麼都不做」,它能做的就只有這麼多了,而不會過於強制。如果你願意,你可以使用 linter 確保處理像這樣的狀況。

關於以真值收窄的最後一點說明是,使用 ! 的布林運算會從否定的分支中濾出。

ts
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
Try

相等收窄

TypeScript 也會使用 switch 陳述式和相等檢查,例如 ===!====!= 來收窄類型。例如

ts
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
(method) String.toUpperCase(): string
y.toLowerCase();
(method) String.toLowerCase(): string
} else {
console.log(x);
(parameter) x: string | number
console.log(y);
(parameter) y: string | boolean
}
}
Try

當我們在上述範例中檢查 xy 是否相等時,TypeScript 知道它們的類型也必須相等。由於 字串xy 都可以採用的唯一共用類型,因此 TypeScript 知道 xy 在第一個分支中必定是 字串

檢查特定字面值(相對於變數)也適用。在我們關於真值收窄的章節中,我們寫了一個 printAll 函式,由於它意外地沒有正確處理空字串,因此容易出錯。我們可以執行特定檢查來封鎖 null,而 TypeScript 仍然會正確地從 strs 的類型中移除 null

ts
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
(parameter) strs: string[]
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
(parameter) strs: string
}
}
}
Try

JavaScript 中使用 ==!= 進行較寬鬆的相等性檢查也會正確地縮小範圍。如果您不熟悉,檢查某個東西 == null 實際上不僅檢查它是否特別是值 null - 它還檢查它是否可能是 undefined。同樣適用於 == undefined:它檢查一個值是否為 nullundefined

ts
interface Container {
value: number | null | undefined;
}
 
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
(property) Container.value: number
 
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
Try

in 運算子縮小範圍

JavaScript 有個運算子用於判斷物件或其原型鏈是否有一個具有名稱的屬性:in 運算子。TypeScript 將此視為縮小潛在類型的一種方式。

例如,使用以下程式碼:"value" in x。其中 "value" 是字串文字,而 x 是聯合類型。「真」分支縮小 x 的類型,這些類型具有可選或必需的屬性 value,而「假」分支縮小到具有可選或遺失屬性 value 的類型。

ts
type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
 
return animal.fly();
}
Try

重申一下,可選屬性將存在於縮小的兩側。例如,人類既可以游泳,又可以飛(使用適當的設備),因此應該出現在 in 檢查的兩側

ts
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
 
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal;
(parameter) animal: Fish | Human
} else {
animal;
(parameter) animal: Bird | Human
}
}
Try

instanceof narrowing

JavaScript 有個用於檢查值是否為另一個值的「實例」的運算子。更具體地說,在 JavaScript 中,x instanceof Foo 會檢查 x原型鏈是否包含 Foo.prototype。雖然我們在此不會深入探討,而且當我們進入類別時,你會看到更多,但它們仍然可用於大多數可以使用 new 建構的值。正如你可能猜到的,instanceof 也是一個類型守衛,而 TypeScript 會在受 instanceof 守衛的分支中縮小範圍。

ts
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
(parameter) x: Date
} else {
console.log(x.toUpperCase());
(parameter) x: string
}
}
Try

指派

正如我們先前提到的,當我們指派給任何變數時,TypeScript 會查看指派的右側,並適當地縮小左側的範圍。

ts
let x = Math.random() < 0.5 ? 10 : "hello world!";
let x: string | number
x = 1;
 
console.log(x);
let x: number
x = "goodbye!";
 
console.log(x);
let x: string
Try

請注意,這些指定都為有效。即使在我們第一次指定後,x 的觀察類型已變更為 number,我們仍然可以將 string 指定給 x。這是因為 x宣告類型x 開始的類型)為 string | number,而可指定性總是會針對宣告類型進行檢查。

如果我們將 boolean 指定給 x,我們會看到一個錯誤,因為這不是宣告類型的一部分。

ts
let x = Math.random() < 0.5 ? 10 : "hello world!";
let x: string | number
x = 1;
 
console.log(x);
let x: number
x = true;
Type 'boolean' is not assignable to type 'string | number'.2322Type 'boolean' is not assignable to type 'string | number'.
 
console.log(x);
let x: string | number
Try

控制流程分析

到目前為止,我們已經瀏覽了一些 TypeScript 在特定分支中如何縮小的基本範例。但除了從每個變數往上走並在 ifwhile、條件式等中尋找類型防護之外,還有一些事情正在進行中。例如

ts
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
Try

padLeft 從其第一個 if 區塊中傳回。TypeScript 能夠分析此程式碼,並看到在 paddingnumber 的情況下,其餘主體(return padding + input;)是無法到達的。因此,它能夠從 padding 的類型中移除 number(從 string | number 縮小為 string),以供函式使用。

這種基於可及性的程式碼分析稱為控制流程分析,而 TypeScript 會使用此流程分析來縮小類型,因為它會遇到類型防護和指派。分析變數時,控制流程可能會不斷地分開和重新合併,而該變數在每個點都可能被觀察到具有不同的類型。

ts
function example() {
let x: string | number | boolean;
 
x = Math.random() < 0.5;
 
console.log(x);
let x: boolean
 
if (Math.random() < 0.5) {
x = "hello";
console.log(x);
let x: string
} else {
x = 100;
console.log(x);
let x: number
}
 
return x;
let x: string | number
}
Try

使用類型謂詞

到目前為止,我們已經使用現有的 JavaScript 結構來處理縮小,但是有時您希望更直接地控制整個程式碼中類型的變化方式。

若要定義使用者定義的類型防護,我們只需定義一個其回傳類型為類型謂詞的函式

ts
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
Try

pet is Fish 是此範例中的類型謂詞。謂詞採用 parameterName is Type 的形式,其中 parameterName 必須是目前函式簽章中參數的名稱。

任何時候都使用 isFish 呼叫某些變數,如果原始類型相容,TypeScript 會將該變數縮小到該特定類型。

ts
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
 
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Try

請注意,TypeScript 不僅知道 petif 分支中是 Fish;它還知道在 else 分支中,您沒有 Fish,所以您必須有 Bird

您可以使用類型防護 isFish 來篩選 Fish | Bird 陣列並取得 Fish 陣列

ts
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
 
// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "sharkey") return false;
return isFish(pet);
});
Try

此外,類別可以使用 this is Type 來縮小其類型。

斷言函式

類型也可以使用 斷言函式 來縮小。

辨別聯合

到目前為止,我們看過的大多數範例都集中在使用簡單類型(例如 stringbooleannumber)縮小單一變數。雖然這很常見,但我們在 JavaScript 中大部分時間會處理稍微更複雜的結構。

為了激發動機,讓我們想像我們正在嘗試編碼形狀,例如圓形和正方形。圓形追蹤其半徑,而正方形追蹤其邊長。我們將使用一個名為 kind 的欄位來告訴我們正在處理哪個形狀。以下是定義 Shape 的第一次嘗試。

ts
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
Try

請注意,我們使用字串文字類型的聯集:"circle""square" 來告訴我們是否應分別將形狀視為圓形或正方形。透過使用 "circle" | "square" 代替 string,我們可以避免拼寫錯誤問題。

ts
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
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.
// ...
}
}
Try

我們可以撰寫一個 getArea 函式,根據函式是否處理圓形或正方形來套用正確的邏輯。我們將先嘗試處理圓形。

ts
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.
}
Try

strictNullChecks 下,這會給我們一個錯誤,這是適當的,因為 radius 可能未定義。但如果我們對 kind 屬性執行適當的檢查,會如何?

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
'shape.radius' is possibly 'undefined'.18048'shape.radius' is possibly 'undefined'.
}
}
Try

嗯,TypeScript 仍然不知道這裡該怎麼做。我們已經達到一個我們比類型檢查器更了解我們的值的點。我們可以嘗試使用非空斷言(shape.radius 後面的 !)來說明 radius 絕對存在。

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}
Try

但這感覺不太理想。我們必須對類型檢查器大聲喊叫,使用那些非空斷言 (!) 來說服它 shape.radius 已定義,但如果我們開始移動程式碼,這些斷言容易出錯。此外,在 strictNullChecks 之外,我們無論如何都可以意外存取其中任何欄位(因為在讀取時,假設選用屬性始終存在)。我們絕對可以做得更好。

使用這種編碼 Shape 的問題在於,類型檢查器無法根據 kind 屬性知道 radiussideLength 是否存在。我們需要將我們知道的事情傳達給類型檢查器。有鑑於此,讓我們再試一次定義 Shape

ts
interface Circle {
kind: "circle";
radius: number;
}
 
interface Square {
kind: "square";
sideLength: number;
}
 
type Shape = Circle | Square;
Try

在這裡,我們已適當地將 Shape 分離為兩個類型,kind 屬性具有不同的值,但 radiussideLength 在各自的類型中宣告為必填屬性。

當我們嘗試存取 Shaperadius 時,讓我們看看這裡會發生什麼事。

ts
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
Property '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'.
}
Try

與我們對 Shape 的第一個定義一樣,這仍然是一個錯誤。當 radius 為選用時,我們會收到一個錯誤(已啟用 strictNullChecks),因為 TypeScript 無法判斷該屬性是否存在。現在 Shape 是一個聯合,TypeScript 告訴我們 shape 可能是一個 Square,而 Square 上沒有定義 radius!兩種解釋都是正確的,但只有 Shape 的聯合編碼會導致錯誤,而不管 strictNullChecks 如何設定。

但是,如果我們再次嘗試檢查 kind 屬性呢?

ts
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
}
}
Try

這樣可以消除錯誤!當聯合中的每個類型都包含具有文字類型的共用屬性時,TypeScript 會將其視為辨別聯合,並可以縮小聯合的成員。

在此情況下,kind 是共用屬性(這就是 Shape辨別屬性)。檢查 kind 屬性是否為 "circle" 會消除 Shape 中沒有類型為 "circle"kind 屬性的所有類型。這將 shape 縮小到類型 Circle

相同的檢查也適用於 switch 陳述式。現在,我們可以嘗試撰寫完整的 getArea,而不會有任何令人討厭的 ! 非空斷言。

ts
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
case "square":
return shape.sideLength ** 2;
(parameter) shape: Square
}
}
Try

這裡的重要事項是 Shape 的編碼。向 TypeScript 傳達正確的資訊 - CircleSquare 實際上是兩個具有特定 kind 欄位的不同類型 - 至關重要。這樣做讓我們可以撰寫類型安全的 TypeScript 程式碼,其看起來與我們原本會撰寫的 JavaScript 沒有什麼不同。從那裡,類型系統能夠執行「正確」的事情,並找出 switch 陳述式中每個分支中的類型。

順便一提,試著玩玩上面的範例,並移除一些 return 關鍵字。你會發現,在意外地落入 switch 陳述式中的不同子句時,類型檢查可以幫助避免錯誤。

辨別聯合不僅可用於討論圓形和正方形。它們很適合用於表示 JavaScript 中的任何訊息傳遞機制,例如透過網路傳送訊息(用戶端/伺服器通訊),或編碼狀態管理架構中的變異。

never 類型

在縮小時,你可以將聯合選項減少到移除所有可能性且一無所有的程度。在這種情況下,TypeScript 將使用 never 類型來表示不應存在的狀態。

窮盡性檢查

never 類型可指定給每個類型;但是,沒有類型可指定給 never(除了 never 本身)。這表示你可以使用縮小並依賴 never 出現以在 switch 陳述式中進行窮盡性檢查。

例如,在我們的 getArea 函式中新增一個 default,嘗試將形狀指定給 never,在處理完所有可能的情況時,不會產生錯誤。

ts
type Shape = Circle | Square;
 
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
Try

Shape 聯合中新增一個新成員,將導致 TypeScript 錯誤

ts
interface Triangle {
kind: "triangle";
sideLength: number;
}
 
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
Type 'Triangle' is not assignable to type 'never'.2322Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
Try

TypeScript 文件是一個開放原始碼專案。協助我們改善這些頁面 透過傳送 Pull Request

此頁面的貢獻者
RCRyan Cavanaugh (52)
OTOrta Therox (15)
SBSiarhei Bobryk (2)
ABAndrew Branch (2)
DRDaniel Rosenwasser (2)
26+

最後更新:2024 年 3 月 21 日