TypeScript 類型斷言與 as const
有時候你比 TypeScript 更了解某個值的類型。類型斷言讓你告訴編譯器「相信我」。
一、 類型斷言基礎
1.1 為什麼需要斷言?
typescript
// TypeScript 不知道 getElementById 回傳什麼元素
const canvas = document.getElementById('myCanvas')
// 類型:HTMLElement | null
// 你知道它是 canvas,但 TypeScript 不知道
// canvas.getContext('2d') // 錯誤!
// 使用斷言
const canvas2 = document.getElementById('myCanvas') as HTMLCanvasElement
canvas2.getContext('2d') //1.2 兩種語法
typescript
// 語法一:as(推薦)
const canvas = document.getElementById('myCanvas') as HTMLCanvasElement
// 語法二:尖括號(在 JSX 中不能用)
const canvas = <HTMLCanvasElement>document.getElementById('myCanvas')TIP
推薦使用 as 語法,因為它在 JSX/TSX 中也能使用。
二、 常見使用場景
2.1 DOM 元素
typescript
const input = document.querySelector('input') as HTMLInputElement
console.log(input.value)
const form = document.getElementById('form') as HTMLFormElement;
form.submit();
const btn = document.querySelector('.btn') as HTMLButtonElement
btn.disabled = true2.2 事件處理
typescript
function handleClick(event: Event) {
const target = event.target as HTMLButtonElement
console.log(target.textContent)
}
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}2.3 API 回應
typescript
interface User {
id: number
name: string
}
async function fetchUser() {
const response = await fetch('/api/user')
const data = (await response.json()) as User
return data
}三、 非空斷言(!)
3.1 基本用法
typescript
// TypeScript 認為可能是 null
const element = document.getElementById('app')
// 類型:HTMLElement | null
// 使用非空斷言
const element2 = document.getElementById('app')!
// 類型:HTMLElement
// 或在使用時斷言
element!.textContent = 'Hello'3.2 可選鏈 vs 非空斷言
typescript
interface User {
name: string
address?: {
city: string
}
}
const user: User = { name: 'John' }
// 可選鏈(安全)
console.log(user.address?.city) // undefined
// 非空斷言(危險!如果 address 是 undefined 會報錯)
// console.log(user.address!.city) // Runtime Error!WARNING
非空斷言很危險!只在你 100% 確定值存在時使用。
四、 const 斷言
4.1 基本用法
typescript
// 沒有 as const
const config = {
api: 'https://api.example.com',
timeout: 5000,
}
// 類型:{ api: string; timeout: number }
// 使用 as const
const config2 = {
api: 'https://api.example.com',
timeout: 5000,
} as const
// 類型:{ readonly api: 'https://api.example.com'; readonly timeout: 5000 }4.2 陣列與 as const
typescript
// 沒有 as const
const colors = ['red', 'green', 'blue']
// 類型:string[]
// 使用 as const
const colors2 = ['red', 'green', 'blue'] as const
// 類型:readonly ['red', 'green', 'blue']
// 可以用來建立聯合類型
type Color = (typeof colors2)[number] // 'red' | 'green' | 'blue'4.3 函式參數
typescript
function request(config: { method: 'GET' | 'POST'; url: string }) {
// ...
}
// 沒有 as const
const config = { method: 'GET', url: '/api' }
// request(config) // 錯誤!method 是 string 不是 'GET' | 'POST'
// 使用 as const
const config2 = { method: 'GET', url: '/api' } as const
request(config2) // OK五、 雙重斷言
5.1 何時需要?
typescript
// 有時候 TypeScript 不允許直接斷言
const x = 'hello' as number // 錯誤!string 不能斷言為 number
// 使用雙重斷言(先斷言為 unknown)
const y = 'hello' as unknown as number // 可以,但很危險!5.2 為什麼要避免?
typescript
// 雙重斷言繞過了類型檢查
const user = { name: 'John' } as unknown as { id: number }
// user.id 是 undefined,但 TypeScript 認為是 number!
console.log(user.id.toFixed()) // Runtime Error!CAUTION
雙重斷言極度危險,幾乎永遠不該使用。如果你需要它,說明類型設計有問題。
六、 satisfies 運算子
6.1 基本用法(TypeScript 4.9+)
typescript
// 傳統方式:失去字面量類型
const config: Record<string, string | number> = {
api: 'https://api.example.com',
timeout: 5000,
}
config.api.toUpperCase() // 錯誤!可能是 number
// 使用 satisfies:保留字面量類型
const config2 = {
api: 'https://api.example.com',
timeout: 5000,
} satisfies Record<string, string | number>
config2.api.toUpperCase() // OK,api 確定是 string
config2.timeout.toFixed() // OK,timeout 確定是 number6.2 vs 類型標註
typescript
type Colors = Record<string, [number, number, number]>
// 類型標註:失去鍵的字面量類型
const colors: Colors = {
red: [255, 0, 0],
green: [0, 255, 0],
}
// colors.red 的類型是 [number, number, number]
// colors.blue 也不會報錯(undefined)
// satisfies:保留鍵的類型
const colors2 = {
red: [255, 0, 0],
green: [0, 255, 0],
} satisfies Colors
// colors2.red 可以正常存取
// colors2.blue // 錯誤!不存在七、 最佳實踐
7.1 避免過度使用斷言
typescript
// 過度斷言
const user = {} as User
user.name = 'John' // 危險!
// 正確做法
const user: User = {
name: 'John',
email: 'john@example.com',
}7.2 使用類型守衛替代
typescript
function process(value: unknown) {
// 直接斷言
const str = value as string
// 類型守衛
if (typeof value === 'string') {
console.log(value.toUpperCase())
}
}7.3 必要時才用斷言
typescript
// DOM 操作是斷言的合理場景
const canvas = document.getElementById('canvas') as HTMLCanvasElement
// API 回應也是合理場景(配合驗證更好)
async function fetchUser(): Promise<User> {
const response = await fetch('/api/user')
const data = await response.json()
// 最好加上驗證
if (isUser(data)) {
return data
}
throw new Error('Invalid data')
}總結
| 語法 | 用途 | 安全性 |
|---|---|---|
as Type | 類型斷言 | 中 |
! | 非空斷言 | 低 |
as const | 常量斷言 | 高 |
as unknown as T | 雙重斷言 | 極低 |
satisfies | 類型檢查 + 保留推論 | 高 |
> **使用原則**:
- 優先使用類型守衛
- 必要時才用斷言
- 避免非空斷言和雙重斷言
- 善用
as const和satisfies
進階挑戰
- 為
querySelector加上正確的類型斷言:
typescript
const form = document.querySelector('#login-form');
const input = document.querySelector('input[name='email']')
const button = document.querySelector('button[type='submit']')- 使用
as const建立一個設定物件,並從中提取類型:
typescript
const STATUS = {
PENDING: 'pending',
SUCCESS: 'success',
ERROR: 'error'
} as const
type Status = ??? // 提取聯合類型