TypeScript 變數與常數宣告 – Narrowing(型別縮小)
簡介
在 TypeScript 中,型別縮小(Narrowing) 是編譯器根據程式的控制流程,將一個較寬鬆的聯合型別(union type)或 any 型別「縮」成更具體的型別。這項機制讓開發者在寫程式時可以保有彈性,同時在編譯階段獲得更精確的型別檢查與自動完成。
對於 變數與常數的宣告,正確運用型別縮小不僅能避免執行時的錯誤,還能提升程式碼的可讀性與維護性。本文將從概念說明、實作範例、常見陷阱到實務應用,帶你一步步掌握 TypeScript 的型別縮小技巧。
核心概念
1. 為什麼需要型別縮小?
- 靜態安全:在執行前就能捕捉到不合法的屬性存取或方法呼叫。
- 開發體驗:IDE 能根據縮小後的型別提供正確的提示與自動完成。
- 程式可讀:透過條件分支明確表達「此時變數一定是 X 型別」的意圖。
2. 基本的縮小方式
| 條件 | 會觸發的縮小 | 範例 |
|---|---|---|
typeof 判斷 |
基本原始型別(string、number、boolean、symbol、bigint、undefined) |
if (typeof x === "string") { … } |
instanceof 判斷 |
類別或建構子 | if (obj instanceof Date) { … } |
in 判斷 |
物件屬性存在性 | if ("length" in value) { … } |
相等比較 (===、!==) |
具體值的縮小 | if (status === "success") { … } |
真值判斷 (if (value)) |
排除 null、undefined、0、""、false 等 |
if (user) { … } |
判斷陣列 (Array.isArray) |
確認是陣列 | if (Array.isArray(data)) { … } |
自訂型別守護 (value is Type) |
任意複雜條件 | function isPerson(v: any): v is Person { … } |
下面分別說明每種情況,並提供實作範例。
3. typeof 型別守護
typeof 只能辨識 JavaScript 的原始型別,對於 object、function 只能得到 "object" 或 "function",因此常與其他守護結合使用。
function logValue(value: string | number) {
if (typeof value === "string") {
// 這裡 value 已被縮小為 string
console.log(`字串長度:${value.length}`);
} else {
// 這裡 value 為 number
console.log(`數值加 10:${value + 10}`);
}
}
技巧:
typeof比較時請使用字串常數(如"string"),避免寫成變數造成編譯錯誤。
4. instanceof 判斷類別
instanceof 能辨識由 class 或建構子函式產生的實例。
class Cat {
meow() { console.log("喵~"); }
}
class Dog {
bark() { console.log("汪~"); }
}
function speak(animal: Cat | Dog) {
if (animal instanceof Cat) {
// animal 被縮小為 Cat
animal.meow();
} else {
// animal 為 Dog
animal.bark();
}
}
注意:跨 iframe 或不同執行環境時,
instanceof可能失效,這時可改用Object.prototype.toString.call或自訂守護函式。
5. in 操作符檢查屬性
in 可用於判斷物件是否擁有特定屬性,常與 discriminated union(辨別聯合型別)搭配。
type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
type Shape = Square | Circle;
function area(shape: Shape) {
if ("size" in shape) {
// shape 為 Square
return shape.size ** 2;
} else {
// shape 為 Circle
return Math.PI * shape.radius ** 2;
}
}
小提醒:
in只能檢查 可列舉(enumerable)的屬性,對於 Symbol 或不可列舉屬性無效。
6. 相等比較的縮小
直接比較具體字面值或 enum 成員時,TypeScript 會根據比較結果縮小型別。
enum Status { Success = "success", Fail = "fail" }
function handle(status: Status) {
if (status === Status.Success) {
// status 為 Status.Success
console.log("處理成功");
} else {
// status 為 Status.Fail
console.log("處理失敗");
}
}
7. 真值判斷(Truthiness)
在 if (value) 或 &&、|| 等布林運算中,TypeScript 會排除 falsy(null、undefined、0、NaN、""、false)的可能性。
function greet(name: string | null | undefined) {
if (name) {
// name 被縮小為 string
console.log(`哈囉,${name}!`);
} else {
console.log("哈囉,陌生人!");
}
}
陷阱:如果變數的型別同時包含
0或"",真值判斷會把它們排除,可能不是你想要的行為。此時可改用name != null只排除null/undefined。
8. 陣列與 Array.isArray
Array.isArray 是判斷值是否為陣列的唯一安全方法,縮小結果會變成 any[] 或原本的元素型別陣列。
function flatten(input: number[] | number[][]) {
if (Array.isArray(input[0])) {
// input 被縮小為 number[][]
return input.flat();
} else {
// input 為 number[]
return input;
}
}
9. 自訂型別守護(User‑defined type guard)
當內建守護不足時,我們可以寫一個返回型別謂詞 (value is Type) 的函式,讓編譯器知道縮小的結果。
interface Person {
name: string;
age: number;
}
function isPerson(value: any): value is Person {
return (
typeof value === "object" &&
value !== null &&
typeof value.name === "string" &&
typeof value.age === "number"
);
}
function printInfo(obj: Person | string) {
if (isPerson(obj)) {
// obj 被縮小為 Person
console.log(`${obj.name} (${obj.age} 歲)`);
} else {
// obj 為 string
console.log(`字串:${obj}`);
}
}
關鍵:型別守護函式的回傳型別必須寫成
value is Person,而非單純的boolean,否則縮小不會生效。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
使用 any 抹掉縮小 |
若變數被宣告為 any,所有縮小都失效。 |
盡量避免 any,改用 unknown 搭配型別守護。 |
null 與 undefined 同時排除 |
真值判斷會同時排除 0、"" 等,造成意外的型別變化。 |
使用 value != null 或 value !== undefined 只排除需要的情況。 |
跨執行環境的 instanceof |
於不同 window/iframe 時,instanceof 可能不相等。 |
改用 Object.prototype.toString.call(value) === "[object Type]" 或自訂守護。 |
in 判斷可列舉屬性 |
in 無法檢測 Symbol 或不可列舉屬性。 |
若需要檢測 Symbol,使用 Object.getOwnPropertySymbols。 |
過度依賴 as 斷言 |
手動斷言會跳過編譯器的縮小檢查,易產生 runtime 錯誤。 | 盡量使用型別守護,而非 as。 |
最佳實踐
- 先用內建守護(
typeof、instanceof、in)再考慮自訂守護。 - 保持變數的最小聯合型別,不要一次把太多型別混在一起。
- 使用
unknown取代any,配合守護函式提升安全性。 - 寫測試:即使有型別縮小,仍建議撰寫單元測試驗證邏輯分支。
實際應用場景
API 回傳資料的型別安全
從後端取得的 JSON 可能是User或ErrorResponse,使用 discriminated union 搭配in或kind屬性可安全地取出資料。type ApiResult = { kind: "user"; data: User } | { kind: "error"; message: string }; function handleResult(res: ApiResult) { if (res.kind === "user") { console.log(`使用者名稱:${res.data.name}`); } else { console.error(`錯誤:${res.message}`); } }表單輸入的多型別驗證
表單欄位可能接受字串或數字,利用typeof與自訂守護,保證後續處理時型別正確。React 元件的 Props
某個元件接受string | ReactNode作為子元素,透過typeof判斷可以決定是否直接渲染或包裝。interface Props { content: string | React.ReactNode } const Card: React.FC<Props> = ({ content }) => ( <div className="card"> {typeof content === "string" ? <p>{content}</p> : content} </div> );
總結
- 型別縮小 是 TypeScript 讓靜態型別與動態程式流程相互配合的核心機制。
- 透過
typeof、instanceof、in、相等比較、真值判斷 以及 自訂型別守護,我們可以在不同分支中得到更精確的型別資訊。 - 正確使用縮小能提升 安全性、開發體驗,並降低 runtime 錯誤的風險。
- 在實務開發中,建議先利用內建守護,必要時再撰寫自訂守護,並配合
unknown、測試 與 最佳實踐,打造穩健且易維護的 TypeScript 程式碼。
掌握了型別縮小,你就能在 變數與常數宣告 的每一次選擇中,都保持型別的清晰與安全,寫出更可靠的前端或 Node.js 應用。祝你在 TypeScript 的世界裡玩得開心、寫得順手!