TypeScript – 型別推論與型別保護(Type Inference & Narrowing)
主題:typeof 型別保護
簡介
在 JavaScript 的執行階段,typeof 是最常見的「型別偵測」工具;而在 TypeScript 中,它同時也是 型別保護(type guard) 的核心之一。透過 typeof,編譯器能在條件分支裡 自動縮小(narrow) 變數的型別,使得後續的程式碼取得正確的型別資訊,減少 any 或手動斷言的使用。
對於 初學者,了解 typeof 如何與 TypeScript 的型別推論結合,是寫出安全、可維護程式的第一步。對 中級開發者,則能利用 typeof 進行更精細的型別守護,搭配自訂型別保護或泛型,打造彈性十足的函式庫。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現 typeof 型別保護的使用方式與實務價值。
核心概念
1. typeof 在 JavaScript 與 TypeScript 的差異
| 語言 | 回傳值 | 主要用途 |
|---|---|---|
| JavaScript | "number"、"string"、"boolean"、"undefined"、"object"、"function"、"symbol"、"bigint" |
執行時檢查 |
| TypeScript | 同上,但 同時提供型別縮小 | 編譯時推斷、減少 any |
重點:在 TypeScript 中,
typeof只在 條件式(if、switch、三元運算子)內才會觸發型別縮小。離開條件分支後,變數會恢復原本的寬鬆型別。
2. 基本型別保護:primitive 類型
function double(value: number | string) {
// 依據 typeof 進行型別保護
if (typeof value === "number") {
// 這裡 value 被縮小為 number
return value * 2;
} else {
// 這裡 value 被縮小為 string
return value + value;
}
}
// 測試
console.log(double(10)); // 20
console.log(double("Hi")); // HiHi
說明
value原本是聯合型別number | string。if (typeof value === "number")成立時,編譯器 自動把value視為number,因此可以直接使用算術運算。- 反之,
else分支則被視為string,允許字串相加。
3. 與 null、undefined 搭配使用
null 與 undefined 在 typeof 中分別回傳 "object"(null)與 "undefined",因此在保護它們時常需要額外的檢查。
function format(input: string | null | undefined) {
if (typeof input === "string") {
return input.trim();
}
// 此時 input 仍可能是 null 或 undefined
if (input == null) { // 同時檢查 null 與 undefined
return "";
}
// 這裡永遠不會執行到
return "unreachable";
}
說明
typeof input === "string"只保護字串。input == null(使用寬鬆相等)同時捕捉null與undefined,這是 常見的型別保護技巧。
4. 與函式結合的 typeof 保護
typeof 也能辨識 函式,讓我們在接受多型參數時,安全地呼叫它。
type Callback = ((msg: string) => void) | undefined;
function greet(name: string, cb?: Callback) {
console.log(`Hello, ${name}!`);
if (typeof cb === "function") {
// cb 被縮小為 (msg: string) => void
cb(`Welcome, ${name}`);
}
}
說明
cb可能是undefined,所以先用typeof cb === "function"確認。- 這樣做比
if (cb)更精確,因為cb可能是其他 truthy 值(如0、"")在未來的型別變更中造成誤判。
5. typeof 與 自訂型別保護 的結合
雖然 typeof 只能辨識 primitive 類型與函式,但我們可以 在自訂型別保護裡使用 typeof,讓保護更具彈性。
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(data: Person | string) {
if (isPerson(data)) {
// data 被縮小為 Person
console.log(`${data.name} (${data.age} 歲)`);
} else {
// data 為 string
console.log(`訊息:${data}`);
}
}
說明
isPerson使用typeof檢查object、string、number,同時返回 型別謂詞 (value is Person)。- 在
printInfo中,if (isPerson(data))讓 TypeScript 把data縮小為Person,即使Person本身是一個自訂介面。
常見陷阱與最佳實踐
| 陷阱 | 可能的錯誤 | 正確寫法 |
|---|---|---|
把 null 當作 object |
typeof null === "object" 會誤判為有效物件 |
同時檢查 value !== null |
忽略 bigint、symbol |
只檢查 "number"、"string",導致其他 primitive 被忽略 |
加入 typeof v === "bigint"、"symbol" 的分支 |
使用寬鬆相等 == 判斷 null |
可能在未預期的情況下把 0、false 誤判 |
建議使用 `value === null |
在非條件語句中使用 typeof |
型別不會被縮小,仍是原始聯合型別 | 只在 if、else if、switch、三元運算子等分支內使用 |
把 typeof 結果寫成字面量型別 |
if (typeof x === "number") 之後仍被視為 any(舊版 TS) |
確保 TypeScript 版本 >= 2.0,或使用 as const 斷言結果字串 |
最佳實踐
明確列出所有 primitive
if (typeof v === "string") { … } else if (typeof v === "number") { … } else if (typeof v === "boolean") { … } else if (typeof v === "bigint") { … } else if (typeof v === "symbol") { … } else if (typeof v === "undefined") { … } else if (typeof v === "function") { … } else if (v === null) { … }結合
Array.isArray判斷陣列(typeof只能回傳"object")if (Array.isArray(value)) { … }將保護邏輯抽成函式,提升可讀性與重用性(如
isPerson範例)。使用
never讓未處理的型別在編譯時錯誤function exhaustiveCheck(x: never): never { throw new Error(`未處理的型別: ${x}`); }保持 TypeScript 版本更新,新版會更好地支援
typeof型別保護與型別縮小。
實際應用場景
1. API 回傳的多型資料
假設後端回傳的資料可能是 字串(錯誤訊息)或 物件(成功結果):
type ApiResponse = { data: string[] } | string;
function handleResponse(res: ApiResponse) {
if (typeof res === "string") {
console.error(`API 錯誤:${res}`);
return;
}
// 這裡 res 被縮小為 { data: string[] }
console.log("取得資料:", res.data);
}
2. 事件處理函式
瀏覽器的 Event 物件在某些情況下會是 KeyboardEvent、MouseEvent… 使用 typeof 判斷 函式型別,避免 null 造成的 Runtime Error:
function addListener(
el: HTMLElement,
type: string,
handler?: ((e: Event) => void) | null
) {
if (typeof handler === "function") {
el.addEventListener(type, handler);
} else {
console.warn("未提供有效的事件處理函式");
}
}
3. 多型函式庫的參數驗證
開發一個通用的 深拷貝 函式,需根據參數的 primitive 類型決定拷貝方式:
function deepClone<T>(value: T): T {
if (typeof value !== "object" || value === null) {
// 基本型別或 null,直接回傳
return value;
}
if (Array.isArray(value)) {
return value.map(item => deepClone(item)) as unknown as T;
}
const result: any = {};
for (const key in value) {
result[key] = deepClone((value as any)[key]);
}
return result;
}
總結
typeof在 TypeScript 中不僅是執行時的型別偵測,更是 型別保護 的核心工具,能讓編譯器在條件分支裡自動 縮小變數型別。- 透過
typeof結合null判斷、函式檢查、陣列檢測,我們可以安全地處理number | string | undefined | null等聯合型別,避免any的濫用。 - 常見陷阱包括
null被視為"object"、遺漏bigint/symbol、在非條件語句中使用typeof等;只要遵循 「列舉所有 primitive」、「在條件分支內使用」 的原則,就能寫出可靠的程式碼。 - 在實務上,
typeof型別保護常見於 API 回傳處理、事件監聽、通用函式庫 等情境,配合自訂型別保護(type predicate)可進一步提升程式的可讀性與可維護性。
掌握 typeof 的型別保護技巧,等於在 TypeScript 的型別系統上多了一層安全防護,讓開發者能以更少的斷言、更自然的程式碼,寫出 安全、可預測、易維護 的 JavaScript/TypeScript 應用。祝你在 TypeScript 的旅程中,玩得開心、寫得順手!