TypeScript 類型守衛與類型收窄
當使用聯合類型時,我們需要「收窄」類型才能安全使用。本篇將介紹各種類型守衛技巧。
一、 為什麼需要類型收窄?
typescript
function printValue(value: string | number) {
// value 可能是 string 或 number
// console.log(value.toUpperCase()) // 錯誤!number 沒有 toUpperCase
// 需要先確定類型
if (typeof value === 'string') {
console.log(value.toUpperCase()) // 在這裡 value 是 string
} else {
console.log(value.toFixed(2)) // 在這裡 value 是 number
}
}二、 typeof 類型守衛
2.1 基本用法
typescript
function process(value: string | number | boolean) {
if (typeof value === 'string') {
// value: string
return value.toUpperCase()
}
if (typeof value === 'number') {
// value: number
return value * 2
}
// value: boolean
return value ? 'Yes' : 'No'
}2.2 typeof 可檢測的類型
| typeof 結果 | 適用類型 |
|---|---|
'string' | string |
'number' | number |
'boolean' | boolean |
'symbol' | symbol |
'bigint' | bigint |
'undefined' | undefined |
'function' | Function |
'object' | object, null, 陣列 |
> `typeof null === 'object'` 是 JavaScript 的歷史遺留問題!
typescript
function handle(value: object | null) {
if (typeof value === 'object') {
// value 仍可能是 null!
}
// 正確方式
if (value !== null && typeof value === 'object') {
// value: object
}
}三、 instanceof 類型守衛
3.1 基本用法
typescript
class Dog {
bark() {
console.log('Woof!')
}
}
class Cat {
meow() {
console.log('Meow!')
}
}
function speak(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark() // animal: Dog
} else {
animal.meow() // animal: Cat
}
}3.2 錯誤物件
typescript
function handleError(error: unknown) {
if (error instanceof Error) {
console.log(error.message)
console.log(error.stack)
} else if (typeof error === 'string') {
console.log(error)
} else {
console.log('Unknown error')
}
}3.3 陣列檢測
typescript
function process(value: string | string[]) {
if (Array.isArray(value)) {
// value: string[]
return value.join(', ')
}
// value: string
return value
}四、 in 運算子
4.1 檢查屬性存在
typescript
type Fish = { swim: () => void }
type Bird = { fly: () => void }
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim() // animal: Fish
} else {
animal.fly() // animal: Bird
}
}4.2 區分聯合類型
typescript
type SuccessResponse = {
status: 'success'
data: object
}
type ErrorResponse = {
status: 'error'
error: string
}
type Response = SuccessResponse | ErrorResponse
function handle(response: Response) {
if ('data' in response) {
console.log(response.data) // SuccessResponse
} else {
console.log(response.error) // ErrorResponse
}
}五、 等值比較
5.1 字面量判斷
typescript
type Status = 'loading' | 'success' | 'error'
function render(status: Status) {
if (status === 'loading') {
return 'Loading...'
}
if (status === 'success') {
return 'Done!'
}
// status: 'error'
return 'Error occurred'
}5.2 區分聯合
typescript
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
function getArea(shape: Shape): number {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2
}
return shape.width * shape.height
}六、 自訂類型守衛(Type Predicate)
6.1 基本語法
typescript
// 回傳 value is string 表示「如果回傳 true,則 value 是 string」
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()) // value: string
}
}6.2 複雜類型
typescript
interface User {
name: string
email: string
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value &&
typeof (value as User).name === 'string' &&
typeof (value as User).email === 'string'
)
}
function greet(value: unknown) {
if (isUser(value)) {
console.log(`Hello, ${value.name}!`) // value: User
}
}6.3 陣列過濾
typescript
// 過濾掉 null 和 undefined
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
const values = [1, null, 2, undefined, 3]
const numbers = values.filter(isDefined)
// numbers: number[]6.4 區分聯合
typescript
type Cat = { type: 'cat'; meow: () => void }
type Dog = { type: 'dog'; bark: () => void }
type Animal = Cat | Dog
function isCat(animal: Animal): animal is Cat {
return animal.type === 'cat'
}
function isDog(animal: Animal): animal is Dog {
return animal.type === 'dog'
}
function makeSound(animal: Animal) {
if (isCat(animal)) {
animal.meow()
} else {
animal.bark()
}
}七、 斷言函式(Assertion Function)
7.1 基本語法
typescript
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string')
}
}
function process(value: unknown) {
assertIsString(value)
// 之後 value 被視為 string
console.log(value.toUpperCase()) //
}7.2 非空斷言
typescript
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined')
}
}
function process(user: User | null) {
assertDefined(user)
console.log(user.name) // user: User
}八、 控制流分析
8.1 自動收窄
typescript
function process(value: string | number | null) {
if (value === null) {
return // 早期返回
}
// value: string | number(null 已排除)
if (typeof value === 'string') {
return value.toUpperCase()
}
// value: number(string 已排除)
return value.toFixed(2)
}8.2 窮盡檢查
typescript
type Status = 'idle' | 'loading' | 'success' | 'error'
function handleStatus(status: Status): string {
switch (status) {
case 'idle':
return 'Idle'
case 'loading':
return 'Loading...'
case 'success':
return 'Done!'
case 'error':
return 'Error!'
default:
// 如果漏掉了某個 case,這裡會報錯
const _exhaustive: never = status
return _exhaustive
}
}總結
| 守衛 | 語法 | 適用場景 |
|---|---|---|
| typeof | typeof x === 'string' | 原始類型 |
| instanceof | x instanceof Class | 類別實例 |
| in | 'prop' in obj | 屬性存在 |
| 等值比較 | x === 'value' | 字面量類型 |
| 類型謂詞 | x is Type | 自訂判斷 |
| 斷言函式 | asserts x is Type | 錯誤拋出 |
> **選擇技巧**:
- 原始類型 →
typeof - 類別 →
instanceof - 物件屬性 →
in - 複雜邏輯 → 自訂類型守衛
進階挑戰
- 實作一個
isArray類型守衛 - 為以下 API 回應實作類型守衛:
typescript
type ApiResponse =
| { success: true; data: User }
| { success: false; error: string }
// 實作
function isSuccessResponse(res: ApiResponse): res is ???{
// ...
}- 使用斷言函式確保環境變數存在