型別收窄

想象我們有一個名為 padLeft 的函式。

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

如果 padding 是一個 number,它會將該數字視為我們希望在 input 前面新增的空格數量。如果 padding 是一個 string,它應該直接在 input 前面拼接 padding。讓我們嘗試實現 padLeftpadding 引數為 number 時的邏輯。

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 的值傳遞給了只接受 numberrepeat 函式,它是對的。換句話說,我們沒有先明確檢查 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",並將其理解為一種特殊的程式碼形式,稱為型別保護 (type guard)。TypeScript 會追蹤程式可能採取的執行路徑,以分析在給定位置值的最具體型別。它會檢視這些特殊的檢查(稱為型別保護)和賦值,而將型別細化為比宣告的型別更具體型別的過程被稱為型別縮小 (narrowing)。在許多編輯器中,我們可以觀察這些型別在變化時的過程,我們甚至會在示例中展示這一點。

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 期望它返回一組特定的字串:

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

正如我們在 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[]

這可能是一個很好的過渡,引出我們所謂的“真值”檢查(truthiness checking)。

真值縮小 (Truthiness narrowing)

“真值”(Truthiness)可能不是你在字典裡能找到的詞,但在 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 這樣的結構會首先將它們的條件“強制轉換”(coerce)為 boolean,以便理解它們,然後根據結果是 true 還是 false 選擇分支。諸如

  • 0
  • NaN
  • ""(空字串)
  • 0nbigint 版本的零)
  • 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
This kind of expression is always truthy.2872This kind of expression is always truthy.
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 是否為真值(truthy),我們去除了上面的錯誤。這至少防止了我們在執行程式碼時出現可怕的錯誤,例如:

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

等值縮小 (Equality narrowing)

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 知道它們的型別也必須相等。由於 stringxy 都可能採用的唯一共同型別,TypeScript 知道 xy 在第一個分支中必須是 string

針對特定的字面量值(而不是變數)進行檢查也有效。在我們關於真值縮小的部分中,我們編寫了一個容易出錯的 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:它檢查值是 null 還是 undefined

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 縮小

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

賦值 (Assignments)

正如我們前面提到的,當我們對任何變數進行賦值時,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

控制流分析 (Control flow analysis)

到目前為止,我們已經介紹了一些關於 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;)是不可達的(unreachable)。因此,它能夠在該函式的其餘部分從 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

使用型別謂詞 (Using type predicates)

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

要定義使用者定義的型別保護,我們只需要定義一個返回型別為型別謂詞 (type predicate) 的函式:

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 來縮小它們的型別。

斷言函式 (Assertion functions)

型別也可以使用斷言函式進行縮小。

可辨識聯合 (Discriminated unions)

到目前為止,我們看過的絕大多數示例都集中在縮小具有 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 編碼的問題在於,型別檢查器沒有辦法知道 radiussideLength 是否存在於 kind 屬性的基礎上。我們需要將我們知道的資訊傳達給型別檢查器。考慮到這一點,讓我們再試一次定義 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 欄位的獨立型別——至關重要。這樣做讓我們能夠編寫出看起來與我們本應編寫的 JavaScript 沒有區別的型別安全的 TypeScript 程式碼。從那裡開始,型別系統就能夠做“正確”的事情,並弄清楚我們 switch 語句每個分支中的型別。

順便說一句,嘗試擺弄上面的示例並刪除一些 return 關鍵字。你會發現,在 switch 語句中不小心貫穿(fall through)不同子句時,型別檢查可以幫助避免錯誤。

可辨識聯合的用途不僅僅是討論圓形和正方形。它們非常適合表示 JavaScript 中的任何訊息傳遞方案,例如透過網路傳送訊息(客戶端/伺服器通訊),或在狀態管理框架中對變異進行編碼。

never 型別

在縮小型別時,你可以將聯合的選項減少到移除所有可能性並且什麼都不剩下的程度。在這些情況下,TypeScript 將使用 never 型別來表示不應該存在狀態。

完備性檢查 (Exhaustiveness checking)

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)
28+

最後更新:2026 年 3 月 27 日