union 與 unknown 在型別系統裡好用,但一要呼叫方法,TS 就會逼你「先確定它是什麼」。這篇收齊讓 TS 在 runtime 分流時自動縮型別的工具——typeof、in、instanceof、自訂 type guard、asserts,以及最該優先用的 discriminated union。
所謂「窄化(narrowing)」,就是在某個程式分支裡,TS 把變數的型別從廣的縮小到具體的某一種。
typeof 守衛
最基本的一招,拿來分原始型別:
function pad(value: string | number) {
if (typeof value === 'number') {
return value.toFixed(2) // value: number
}
return value.padStart(5) // value: string
}
typeof 的可能回傳值是 JS 規定的那幾種:'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function'。
注意陣列、null、物件的 typeof 全是 'object',靠它分不出來。
truthiness 守衛
直接拿值當條件,也能窄化:
function f(v: string | null | undefined) {
if (v) {
v.toUpperCase() // v: string
}
}
陷阱在於空字串、0、NaN、false 都是 falsy,會一併被排除。只想擋 null / undefined 的話,改用 == null 或 ??:
if (v != null) { /* string */ } // 同時排掉 null 和 undefined
equality 守衛
兩個變數互相比較,TS 會把雙方都窄化到型別的交集:
function f(a: string | number, b: string | boolean) {
if (a === b) {
// 兩邊型別交集是 string,所以 a, b 都被窄化為 string
a.toUpperCase()
b.toUpperCase()
}
}
跟字面值比較,就窄到那個字面值:
function f(s: 'a' | 'b' | 'c') {
if (s === 'a') {
// s: 'a'
} else {
// s: 'b' | 'c'
}
}
in 守衛
用「有沒有某個屬性」來分物件:
type Fish = { swim: () => void }
type Bird = { fly: () => void }
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim() // Fish
} else {
animal.fly() // Bird
}
}
instanceof 守衛
function logDate(x: Date | string) {
if (x instanceof Date) {
console.log(x.toISOString()) // Date
} else {
console.log(x.toUpperCase()) // string
}
}
只能用在有 constructor 的型別(class、Error、Date 等);純物件字面量沾不上邊。
discriminated union(標籤聯合)
最強大、也最該優先用的窄化模式:讓每個成員帶一個共同的「辨識欄位」(discriminator):
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rect'; w: number; h: number }
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2
case 'square': return s.size ** 2
case 'rect': return s.w * s.h
}
}
s.kind 是字面值 union,TS 在每個 case 裡自動把 s 窄化到對應的分支,連自訂守衛都不用寫。
加上 never 做窮舉檢查
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2
case 'square': return s.size ** 2
case 'rect': return s.w * s.h
default:
const _: never = s // 漏掉 case 這裡會編譯錯
return _
}
}
好處很實際:之後新增一個 Shape 變體卻忘了更新 area,TS 會直接在 default 分支報錯提醒你。
user-defined type guard:is 語法
內建守衛不夠用時,自己寫一個:
interface Cat { meow: () => void }
interface Dog { bark: () => void }
function isCat(x: Cat | Dog): x is Cat {
return (x as Cat).meow !== undefined
}
function play(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow() // Cat
} else {
animal.bark() // Dog
}
}
x is Cat 叫做 type predicate(型別謂詞),是 TS 獨有的語法,意思是「這個函式回 true 時,請把 x 當 Cat」。它只在型別檢查層有意義,編譯到 JS 後整個消失,剩下的就是一個回 boolean 的普通函式。
要小心的是:寫這個語法等於跟 TS 立下承諾「我回 true,x 就真的是 Cat」。TS 不會驗證實作邏輯對不對,謊報了它也照單全收——所以實作裡要誠實。
assertion function:asserts
type predicate 是回 boolean 讓你分流,assertion function 則是不對就直接 throw:
function assertString(x: unknown): asserts x is string {
if (typeof x !== 'string') {
throw new Error('not a string')
}
}
function f(v: unknown) {
assertString(v)
v.toUpperCase() // ✅ 這之後 v 都是 string
}
幾個要點:
- 回傳型別寫成
asserts x is T - 函式必須真的會 throw,否則型別系統就被騙了
- 不能用 arrow function(語法不支援)
型別斷言(as)vs 型別守衛
as | type guard | |
|---|---|---|
| 是否實際檢查 | ❌ 純編譯期 | ✅ 執行期實際判斷 |
| 錯了會發生什麼 | 執行期炸 | 不會錯,分支邏輯正確 |
| 用在何處 | 過渡 / 確定型別但 TS 推不出 | 主要工具 |
const el = document.getElementById('app') as HTMLDivElement // 信任,不檢查
const el2 = document.getElementById('app')
if (el2 instanceof HTMLDivElement) { /* 真的檢查 */ }
non-null 斷言(!)
const el = document.getElementById('app')! // 告訴 TS:絕不為 null
跟 as 一樣是逃生口,不做任何檢查。能用守衛就用守衛。
常見窄化陷阱
1. 在 callback 裡守衛失效
type Box = { value: string | undefined }
function f(box: Box) {
if (box.value !== undefined) {
[1, 2].forEach(() => {
box.value.toUpperCase() // ❌ box.value 又被當 string | undefined
})
}
}
callback 是一個新的 scope,TS 沒辦法保證它執行時 box.value 還沒被改過,所以窄化不帶進去。解法是先存成區域變數:
const v = box.value
if (v !== undefined) {
[1, 2].forEach(() => v.toUpperCase()) // ✅ v 不會被別處改
}
2. 函式呼叫後窄化會「重置」
function check(x: string | null) {
if (x !== null) {
doSomething()
x.toUpperCase() // ✅ 還在窄化範圍內,但若 doSomething 修改外部狀態...
}
}
區域變數通常沒事;物件屬性、外部變數就要小心,呼叫別的函式之後窄化可能不再成立。
3. typeof null === ‘object’
function f(x: object | null) {
if (typeof x === 'object') {
x.foo // ❌ x 仍可能是 null
}
if (x !== null) {
// ✅ 排掉 null 後 x: object
}
}
JS 的老坑:typeof null 也是 'object'。null check 要自己明確寫。
控制流分析
TS 的窄化是基於控制流的,不只看 if 區塊:
function f(v: string | number) {
if (typeof v !== 'string') {
return // 提早 return 窄化掉一個
}
v.toUpperCase() // ✅ v: string
}
if 裡面 return 或 throw 之後,後面的程式自動被窄化——能執行到那裡,就代表前面的條件不成立。
最後一篇進到模組與 .d.ts,把跨檔案 import / export、ambient 宣告、第三方套件型別這些工程面的東西收尾。
Latest Updates
- 2026.06.11 Content updated
