跳至主要內容
Skip to content

TypeScript 聯合類型與交叉類型

聯合類型和交叉類型是 TypeScript 中組合類型的兩種方式。本篇將深入介紹它們的使用場景。


一、 聯合類型(Union Type)

1.1 基本語法

使用 | 表示「或」:

typescript
// 可以是 string 或 number
let id: string | number

id = 'abc' // 
id = 123 // 
// id = true  // 不能是 boolean

1.2 常見用法

typescript
// 狀態字面量
type Status = 'pending' | 'success' | 'error'

// 可選值
type Maybe<T> = T | null | undefined

// 多種輸入
function format(value: string | number): string {
  return String(value)
}

1.3 使用聯合類型

typescript
function printId(id: string | number) {
  // 兩種類型共有的操作
  console.log(id.toString())

  // 不能直接呼叫特定類型的方法
  // console.log(id.toUpperCase())  // 錯誤!number 沒有 toUpperCase

  // 必須先收窄類型
  if (typeof id === 'string') {
    console.log(id.toUpperCase())
  } else {
    console.log(id.toFixed(2))
  }
}

二、 交叉類型(Intersection Type)

2.1 基本語法

使用 & 表示「且」:

typescript
type Person = {
  name: string
  age: number
}

type Employee = {
  employeeId: string
  department: string
}

// 同時具備兩種類型的所有屬性
type EmployeeInfo = Person & Employee

const employee: EmployeeInfo = {
  name: 'John',
  age: 30,
  employeeId: 'E001',
  department: 'Engineering',
}

2.2 多重交叉

typescript
type A = { a: string }
type B = { b: number }
type C = { c: boolean }

type ABC = A & B & C

const abc: ABC = {
  a: 'hello',
  b: 42,
  c: true,
}

2.3 與繼承的差異

typescript
// 繼承(interface)
interface Animal {
  name: string
}

interface Dog extends Animal {
  breed: string
}

// 交叉(type)
type Animal2 = {
  name: string
}

type Dog2 = Animal2 & {
  breed: string
}

// 結果相同,但語法不同

三、 區分聯合類型(Discriminated Unions)

3.1 問題場景

typescript
type Circle = {
  radius: number
}

type Rectangle = {
  width: number
  height: number
}

type Shape = Circle | Rectangle

function getArea(shape: Shape): number {
  // 如何區分是 Circle 還是 Rectangle?
  // if (shape.radius)  // 不安全
}

3.2 添加區分屬性

typescript
type Circle = {
  kind: 'circle' // 區分標記
  radius: number
}

type Rectangle = {
  kind: 'rectangle' // 區分標記
  width: number
  height: number
}

type Shape = Circle | Rectangle

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'rectangle':
      return shape.width * shape.height
  }
}

3.3 API 回應範例

typescript
type SuccessResponse = {
  success: true
  data: User
}

type ErrorResponse = {
  success: false
  error: string
}

type ApiResponse = SuccessResponse | ErrorResponse

function handleResponse(response: ApiResponse) {
  if (response.success) {
    console.log(response.data) // 可以存取 data
  } else {
    console.log(response.error) // 可以存取 error
  }
}

3.4 狀態機

typescript
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error }

function render(state: State) {
  switch (state.status) {
    case 'idle':
      return 'Click to load'
    case 'loading':
      return 'Loading...'
    case 'success':
      return state.data
    case 'error':
      return state.error.message
  }
}

四、 交叉類型衝突

4.1 相同屬性不同類型

typescript
type A = { x: number }
type B = { x: string }

type C = A & B
// x 的類型是 number & string = never

// const c: C = { x: ??? }  // 無法賦值!

4.2 解決衝突

typescript
// 使用 Omit 排除衝突屬性
type A = { x: number; y: number }
type B = { x: string; z: string }

type C = Omit<A, 'x'> & B
// { y: number, x: string, z: string }

五、 實用模式

5.1 可選聯合

typescript
type Response<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }

async function fetchData<T>(url: string): Promise<Response<T>> {
  try {
    const data = await fetch(url).then((r) => r.json())
    return { status: 'success', data }
  } catch (e) {
    return { status: 'error', error: (e as Error).message }
  }
}

5.2 混合類型

typescript
// 函式也是物件
type Counter = {
  (): number // 可呼叫
  count: number // 有屬性
  reset(): void // 有方法
}

function createCounter(): Counter {
  const fn = function () {
    return fn.count++
  } as Counter

  fn.count = 0
  fn.reset = function () {
    fn.count = 0
  }

  return fn
}

const counter = createCounter()
counter() // 0
counter() // 1
counter.count // 2
counter.reset()

5.3 擴展內建類型

typescript
// 擴展 Error
type ApiError = Error & {
  code: string
  statusCode: number
}

function createApiError(
  message: string,
  code: string,
  statusCode: number
): ApiError {
  const error = new Error(message) as ApiError
  error.code = code
  error.statusCode = statusCode
  return error
}

六、 最佳實踐

6.1 使用區分聯合

typescript
// 好:有區分屬性
type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keydown'; key: string }

// 差:無法區分
type Event2 = { x: number; y: number } | { key: string }

6.2 避免過度複雜

typescript
// 過於複雜
type Nightmare = A & B & C & (D | E) & (F | G | H)

// 拆分成更小的類型
type Base = A & B & C
type Variant1 = Base & D & F
type Variant2 = Base & E & G

總結

類型符號含義用途
聯合類型|A 或 B多種可能值
交叉類型&A 且 B組合多個類型
區分聯合kind / type帶有區分屬性精確類型收窄

> **記憶技巧**:

  • | 像管道,任選一個通過
  • & 像連接,全部都要滿足

進階挑戰

  1. 設計一個表單驗證結果類型(成功 / 失敗 + 錯誤訊息)
  2. 設計一個事件系統,包含 click、keydown、scroll 事件
  3. 使用交叉類型組合一個完整的 User 類型
typescript
// 練習 3 起始
type BasicInfo = { name: string; email: string }
type Auth = { token: string; roles: string[] }
type Preferences = { theme: 'light' | 'dark'; language: string }

// 組合成完整的 User
type User = ???

延伸閱讀與資源