本文 AI 產出,尚未審核

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>)使用

最佳實踐小結:

  1. 預設使用 unknown 取代 any,強制縮窄。
  2. 在 錯誤處理、無限迴圈 或 不可能執行的程式碼 中使用 never。
  3. 透過 assertNever 或 exhaustive checks,保證 列舉/聯合型別 的完整性。
  4. 為常見的 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 專案在 型別安全 與 可讀性 兩方面同時升級,成為更可靠的程式碼基礎。祝開發順利!