TypeScript 進階主題與最佳實踐
主題:提示型別錯誤(never / unknown)
簡介
在日常開發中,我們常會遇到「型別不符合」的錯誤訊息。若只依賴 any 來繞過檢查,會失去 TypeScript 最核心的 型別安全 優勢。never 與 unknown 正是兩個專門用來 提示型別錯誤、協助開發者在編譯階段捕捉潛在問題的關鍵工具。
never表示不會有任何值的型別,常用於 不可達程式碼、拋出例外 或 無限迴圈 的情境。unknown則是 安全的 any:它允許接受任何型別的值,但在使用之前必須先 進行型別縮窄(type narrowing),從而避免不必要的 runtime 錯誤。
掌握這兩個型別的正確用法,不僅能提升程式的可讀性與維護性,還能在大型專案中建立更嚴謹的錯誤處理機制。
核心概念
1. never – 永不會出現的值
never 是 TypeScript 中最「底層」的型別。它代表永遠不會有返回值的情況,常見於:
| 用途 | 範例 | 說明 |
|---|---|---|
| 拋出例外 | function fail(msg: string): never { throw new Error(msg); } |
函式會直接拋出錯誤,永遠不會有返回值。 |
| 無限迴圈 | function loop(): never { while (true) {} } |
永遠不會結束的迴圈。 |
| 不可達程式碼 | function exhaustiveCheck(x: never) {} |
用於 exhaustive type checking(詳見下節)。 |
程式碼範例 1:拋出例外的 never
function assertNever(value: never): never {
// 這裡的 value 永遠不會被呼叫到
throw new Error(`Unexpected value: ${value}`);
}
重點:若在
switch或if判斷中遺漏了某個分支,assertNever能在編譯期提醒開發者。
2. unknown – 安全的任意型別
unknown 與 any 最大的差別在於 使用前必須先縮窄。它允許把任何值賦給它,但不能直接對其進行屬性存取或呼叫。
程式碼範例 2:從 API 取得 unknown 資料
async function fetchJson(url: string): Promise<unknown> {
const response = await fetch(url);
return response.json(); // 回傳 unknown
}
// 正確的使用方式:先縮窄
async function useData() {
const data = await fetchJson('/api/user');
if (typeof data === 'object' && data !== null && 'name' in data) {
// 此時 TypeScript 會把 data 推斷為 { name: unknown }
console.log((data as { name: string }).name);
} else {
console.warn('資料格式不符合預期');
}
}
技巧:配合 type guard(自訂型別保護)可以讓
unknown更易於使用。
3. never 與 unknown 的可指派關係
| 從 → 到 | never |
unknown |
|---|---|---|
never |
✅ 可指派 | ✅ 可指派 |
unknown |
❌ 不可指派 | ✅ 可指派 |
| 其他型別 | ✅(只要符合) | ✅(只要符合) |
說明:
never可以賦值給任何型別(因為它永遠不會有值),而unknown只能賦值給any、unknown或在縮窄後的具體型別。
4. Exhaustive checking(完整性檢查)結合 never
當使用 列舉(enum) 或 聯合型別 時,若忘記處理某個成員,編譯器不會自動報錯。透過 never 可以強制檢查。
程式碼範例 3:使用 never 保障列舉的完整性
enum ActionKind {
Add,
Delete,
Update,
}
type Action =
| { kind: ActionKind.Add; payload: number }
| { kind: ActionKind.Delete; id: string }
| { kind: ActionKind.Update; id: string; value: string };
function handleAction(action: Action) {
switch (action.kind) {
case ActionKind.Add:
console.log('Add', action.payload);
break;
case ActionKind.Delete:
console.log('Delete', action.id);
break;
// 如果忘記寫 Update,下面的 assertNever 會在編譯期報錯
default:
return assertNever(action); // 編譯錯誤:type 'Action' is not assignable to type 'never'
}
}
關鍵:
assertNever讓 TypeScript 必須確認action已經涵蓋所有可能的型別,否則會產生編譯錯誤。
5. unknown 與自訂型別保護(type guard)
自訂型別保護是一種函式,返回值型別為 value is SomeType,用來縮窄 unknown。
程式碼範例 4:自訂型別保護
function isUser(obj: unknown): obj is { id: number; name: string } {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
typeof (obj as any).id === 'number' &&
'name' in obj &&
typeof (obj as any).name === 'string'
);
}
// 使用範例
function greet(user: unknown) {
if (isUser(user)) {
// 此時 user 已被縮窄為 { id: number; name: string }
console.log(`Hello, ${user.name}`);
} else {
console.warn('不是合法的使用者物件');
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳實踐 |
|---|---|---|
直接把 any 當 unknown 使用 |
失去編譯期安全檢查 | 避免 使用 any,改用 unknown + type guard |
把 never 用在普通函式的返回型別 |
會讓呼叫端無法取得結果 | 僅在 拋錯、無限迴圈、不可達 場景使用 never |
忽略 assertNever 的回傳值 |
可能讓程式碼仍能編譯,但執行時出錯 | 必須 把 assertNever 放在 default 分支或 else 中 |
在 unknown 上直接存取屬性 |
編譯錯誤,且若使用 as 直接斷言,會失去安全性 |
使用 型別縮窄(typeof、in、instanceof)或 自訂型別保護 |
把 never 混入聯合型別時未考慮其不可取代性 |
可能導致預期外的可指派行為 | 確認 never 只作為 排除型別(例如 Exclude<T, never>)使用 |
最佳實踐小結:
- 預設使用
unknown取代any,強制縮窄。 - 在 錯誤處理、無限迴圈 或 不可能執行的程式碼 中使用
never。 - 透過
assertNever或 exhaustive checks,保證 列舉/聯合型別 的完整性。 - 為常見的
unknown結構撰寫 型別保護函式,提升可讀性與重用性。
實際應用場景
1. 安全的 JSON 解析
function safeParse(json: string): unknown {
try {
return JSON.parse(json);
} catch {
return undefined; // 仍是 unknown
}
}
// 使用型別保護
function isConfig(obj: unknown): obj is { apiUrl: string; timeout: number } {
return (
typeof obj === 'object' &&
obj !== null &&
'apiUrl' in obj &&
typeof (obj as any).apiUrl === 'string' &&
'timeout' in obj &&
typeof (obj as any).timeout === 'number'
);
}
const raw = '{"apiUrl":"https://example.com","timeout":3000}';
const parsed = safeParse(raw);
if (isConfig(parsed)) {
// 完全安全地使用
console.log(`API: ${parsed.apiUrl}, timeout: ${parsed.timeout}`);
} else {
console.error('設定檔格式錯誤');
}
2. Exhaustive switch 於 Redux Action
在大型前端專案中,Redux 的 action 常以聯合型別呈現。若未處理所有 kind,執行時會出現未預期的行為。使用 never 可以在編譯期捕捉遺漏。
type TodoAction =
| { type: 'ADD'; payload: string }
| { type: 'REMOVE'; id: number }
| { type: 'TOGGLE'; id: number };
function todoReducer(state: string[], action: TodoAction): string[] {
switch (action.type) {
case 'ADD':
return [...state, action.payload];
case 'REMOVE':
return state.filter((_, i) => i !== action.id);
case 'TOGGLE':
// 假設此處尚未實作
return assertNever(action); // 編譯會提醒缺少實作
default:
return assertNever(action);
}
}
3. API 回傳型別的安全包裝
type ApiResponse<T> = { success: true; data: T } | { success: false; error: unknown };
function handleResponse<T>(resp: ApiResponse<T>) {
if (resp.success) {
console.log('Data:', resp.data);
} else {
// unknown 必須先縮窄才能使用
if (resp.error instanceof Error) {
console.error('Error:', resp.error.message);
} else {
console.error('未知錯誤', resp.error);
}
}
}
總結
never與unknown是 TypeScript 型別系統 中的兩把「安全之劍」:never用於 永不會返回 的情境,unknown則是 安全的 any。- 正確使用
never可以 保證程式碼的不可達性,尤其在 exhaustive checking 時發揮關鍵作用。 unknown必須 先縮窄,透過typeof、in、instanceof或 自訂型別保護,才能安全地存取屬性或呼叫方法。- 在實務開發中,將
any換成unknown、搭配assertNever、型別保護與完整性檢查,可顯著降低 runtime 錯誤、提升程式碼可維護性。
掌握這兩個型別的概念與最佳實踐,將讓你的 TypeScript 專案在 型別安全 與 可讀性 兩方面同時升級,成為更可靠的程式碼基礎。祝開發順利!