TypeScript 聯合類型與交叉類型
聯合類型和交叉類型是 TypeScript 中組合類型的兩種方式。本篇將深入介紹它們的使用場景。
一、 聯合類型(Union Type)
1.1 基本語法
使用 | 表示「或」:
typescript
// 可以是 string 或 number
let id: string | number
id = 'abc' //
id = 123 //
// id = true // 不能是 boolean1.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 | 帶有區分屬性 | 精確類型收窄 |
> **記憶技巧**:
|像管道,任選一個通過&像連接,全部都要滿足
進階挑戰
- 設計一個表單驗證結果類型(成功 / 失敗 + 錯誤訊息)
- 設計一個事件系統,包含 click、keydown、scroll 事件
- 使用交叉類型組合一個完整的 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 = ???