跳至主要內容
Skip to content

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
  }
}

總結

守衛語法適用場景
typeoftypeof x === 'string'原始類型
instanceofx instanceof Class類別實例
in'prop' in obj屬性存在
等值比較x === 'value'字面量類型
類型謂詞x is Type自訂判斷
斷言函式asserts x is Type錯誤拋出

> **選擇技巧**:

  • 原始類型 → typeof
  • 類別 → instanceof
  • 物件屬性 → in
  • 複雜邏輯 → 自訂類型守衛

進階挑戰

  1. 實作一個 isArray 類型守衛
  2. 為以下 API 回應實作類型守衛:
typescript
type ApiResponse =
  | { success: true; data: User }
  | { success: false; error: string }

// 實作
function isSuccessResponse(res: ApiResponse): res is ???{
  // ...
}
  1. 使用斷言函式確保環境變數存在

延伸閱讀與資源