TypeScript
單元:變數與常數宣告(Variables)
主題:Type Guards(型別保護)
簡介
在 JavaScript 中,變數的型別是動態決定的,這讓程式在執行時容易出現不可預期的錯誤。TypeScript 透過 靜態型別檢查,在編譯階段就能捕捉大部份問題,但仍有一些情況是 型別不確定(any、聯合型別 string | number 等),此時若直接使用變數的屬性或方法,編譯器會發出警告。
型別保護(Type Guard) 正是解決這類問題的關鍵技術:它讓 TypeScript 在程式碼的特定分支中「窄化」變數的型別,使編譯器能正確推斷後續的操作是否合法。掌握型別保護,不僅能提升程式的安全性,也能讓 IDE 提供更完整的自動完成與即時錯誤提示,對於 初學者到中級開發者 都是必備的技巧。
核心概念
1. 為什麼需要型別保護?
當變數的型別是 聯合型別(Union Type)時,TypeScript 只能保證它同時符合所有成員型別的共同屬性。例如:
let value: string | number;
此時 value 只允許使用 兩者共有 的方法(如 toString()),若直接呼叫 value.toFixed()(只屬於 number)編譯器會報錯。透過型別保護,我們可以在程式的某個分支裡 確認 value 真的是 number,從而安全地使用 toFixed()。
2. 內建型別保護方式
2.1 typeof
typeof 是最常見的保護手段,適用於原始型別(string、number、boolean、symbol、bigint、undefined):
function format(value: string | number) {
if (typeof value === "string") {
// 這裡 TypeScript 會把 value 窄化為 string
return value.toUpperCase();
} else {
// 這裡 value 被視為 number
return value.toFixed(2);
}
}
2.2 instanceof
instanceof 用於判斷 物件是否為某個類別的實例,常見於自訂類別或內建建構子(Date、RegExp):
class Person {
constructor(public name: string) {}
}
class Animal {
constructor(public species: string) {}
}
function greet(entity: Person | Animal) {
if (entity instanceof Person) {
// entity 被窄化為 Person
console.log(`Hello, ${entity.name}`);
} else {
// entity 為 Animal
console.log(`Hello, ${entity.species}`);
}
}
2.3 in 操作符
in 可以檢查 屬性是否存在於物件,常用於「結構型別」的保護:
type Square = { kind: "square"; size: number };
type Circle = { kind: "circle"; radius: number };
function area(shape: Square | Circle) {
if ("size" in shape) {
// shape 被視為 Square
return shape.size ** 2;
} else {
// shape 為 Circle
return Math.PI * shape.radius ** 2;
}
}
3. 自訂型別保護(User‑Defined Type Guard)
有時內建的保護不足以描述複雜的條件,這時可以 自行撰寫型別保護函式。自訂型別保護的寫法必須回傳 value is Type,這是 TypeScript 的 型別謂詞(type predicate)語法。
interface Dog {
kind: "dog";
bark(): void;
}
interface Cat {
kind: "cat";
meow(): void;
}
/** 判斷是否為 Dog */
function isDog(pet: Dog | Cat): pet is Dog {
return pet.kind === "dog";
}
function talk(pet: Dog | Cat) {
if (isDog(pet)) {
// pet 被窄化為 Dog
pet.bark();
} else {
// pet 為 Cat
pet.meow();
}
}
重點:型別謂詞必須放在函式的回傳型別位置,且 不能 使用
any、unknown等會失去型別資訊的型別。
4. 判別聯合(Discriminated Unions)
當每個成員型別都有一個 唯一的字面值屬性(常稱 kind、type),TypeScript 能自動根據此屬性進行型別縮小,這叫 判別聯合。它是最簡潔、最安全的型別保護手法之一。
type Shape =
| { kind: "rectangle"; width: number; height: number }
| { kind: "circle"; radius: number }
| { kind: "triangle"; base: number; height: number };
function perimeter(s: Shape) {
switch (s.kind) {
case "rectangle":
return 2 * (s.width + s.height);
case "circle":
return 2 * Math.PI * s.radius;
case "triangle":
return s.base + s.height + Math.hypot(s.base, s.height);
}
}
在 switch 或 if 中檢查 s.kind 後,TypeScript 立即知道 s 的具體型別,不需要額外的 as 斷言。
5. 多層型別保護的範例
下面示範 結合 typeof、instanceof、自訂保護,處理一個可能是字串、Date、或自訂 LogEntry 物件的參數:
class LogEntry {
constructor(public message: string, public level: "info" | "warn" | "error") {}
}
/** 判斷是否為 LogEntry */
function isLogEntry(v: unknown): v is LogEntry {
return v instanceof LogEntry;
}
function process(input: string | Date | LogEntry) {
if (typeof input === "string") {
console.log("文字訊息:", input);
} else if (input instanceof Date) {
console.log("日期:", input.toISOString());
} else if (isLogEntry(input)) {
// 在這裡 input 已被窄化為 LogEntry
console.log(`[${input.level.toUpperCase()}] ${input.message}`);
} else {
// 這條路徑理論上不會到,除非加入 unknown
console.error("未知型別");
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的做法 |
|---|---|---|
忘記回傳 value is Type |
自訂保護函式若只回傳 boolean,型別不會被縮小。 |
使用 型別謂詞 (param is SpecificType) 作為回傳型別。 |
過度使用 as 斷言 |
as 會直接告訴編譯器「我知道它是什麼」,容易掩蓋錯誤。 |
優先使用 內建保護 或 自訂型別保護,僅在確定安全且無法寫保護時才使用 as。 |
in 操作符檢查不存在的屬性 |
若屬性在某些型別中是可選的,in 仍會回傳 true,導致型別錯誤。 |
結合 hasOwnProperty 或 自訂保護 進一步驗證屬性型別。 |
| 判別聯合缺少唯一屬性 | 若每個成員沒有唯一的字面值屬性,TypeScript 無法自動縮小。 | 為每個子型別加入 kind / type 等 辨識屬性,或使用自訂保護。 |
| 在函式內部重新賦值破壞保護 | 保護後的變數若被重新賦值為寬鬆型別,後續的型別推斷會失效。 | 使用 const 或 let 並避免在保護區塊外改變變數型別。 |
最佳實踐:
- 先使用內建保護(
typeof、instanceof、in),它們最直接且易於閱讀。 - 若內建保護不足,撰寫自訂型別保護函式,並以 型別謂詞 為返回型別。
- 儘量 採用判別聯合 的寫法,讓型別縮小成為語言層面的保證。
- 保持 函式的單一職責:型別保護只負責決定型別,真正的業務邏輯應該在保護之後的分支裡完成。
- 使用 嚴格模式 (
"strict": true),這樣 TypeScript 會在缺少保護的情況下給出更明確的錯誤提示。
實際應用場景
| 場景 | 為何需要型別保護 | 範例概念 |
|---|---|---|
| API 回傳的資料可能是成功或錯誤物件 | 回傳型別為 `Success | Failure`,必須先判斷是哪一種才可安全存取屬性。 |
| 表單輸入值可能是字串或數字 | 依據不同型別執行不同的驗證規則(長度 vs 數值範圍)。 | typeof value === "string" → 正則驗證;else → 數值比較。 |
| 混合型別的 UI 元件屬性 | 某個屬性可接受 string、ReactNode 或 (() => JSX.Element),渲染時必須根據型別分支。 |
if (typeof prop === "function") { ... } else if (React.isValidElement(prop)) { ... } |
| 日誌系統接受多種訊息格式 | 訊息可能是純文字、錯誤物件或自訂 LogEntry,不同型別需要不同的處理流程。 |
參考上面的 process 範例。 |
第三方函式庫回傳 unknown |
unknown 必須先經過型別保護才能被使用,否則會失去型別安全。 |
if (isMyType(value)) { ... },其中 isMyType 為自訂保護。 |
在這些情況下,型別保護不僅避免執行時錯誤,同時讓 IDE 能夠提供更精確的自動完成與即時檢查,提升開發效率與程式碼可讀性。
總結
- 型別保護 是 TypeScript 中讓編譯器在特定程式分支「窄化」變數型別的核心機制。
- 內建的
typeof、instanceof、in能快速處理原始型別、類別實例與結構型別。 - 自訂型別保護(使用型別謂詞)允許我們針對更複雜的判斷條件提供安全的型別縮小。
- 判別聯合(Discriminated Unions)是最簡潔、最具可維護性的做法,只要每個子型別都有唯一的字面值屬性。
- 正確使用型別保護可避免
any/unknown帶來的隱藏錯誤,提升程式的 安全性、可讀性與 IDE 支援。 - 實務上,從 API 回傳、表單驗證、UI 屬性到日誌系統,都能透過型別保護寫出更穩定、易於維護的程式碼。
掌握型別保護,就是在 TypeScript 的型別系統裡,為程式碼加上一層「安全護欄」。只要多加練習、善用內建與自訂的保護手段,你的程式碼將會變得更健壯、更易於團隊協作。祝你在 TypeScript 的旅程中寫出乾淨、可靠的程式!