本文 AI 產出,尚未審核

TypeScript 進階主題與最佳實踐

主題:提示型別錯誤(never / unknown


簡介

在日常開發中,我們常會遇到「型別不符合」的錯誤訊息。若只依賴 any 來繞過檢查,會失去 TypeScript 最核心的 型別安全 優勢。neverunknown 正是兩個專門用來 提示型別錯誤、協助開發者在編譯階段捕捉潛在問題的關鍵工具。

  • 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}`);
}

重點:若在 switchif 判斷中遺漏了某個分支,assertNever 能在編譯期提醒開發者。


2. unknown – 安全的任意型別

unknownany 最大的差別在於 使用前必須先縮窄。它允許把任何值賦給它,但不能直接對其進行屬性存取或呼叫。

程式碼範例 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. neverunknown 的可指派關係

從 → 到 never unknown
never ✅ 可指派 ✅ 可指派
unknown ❌ 不可指派 ✅ 可指派
其他型別 ✅(只要符合) ✅(只要符合)

說明never 可以賦值給任何型別(因為它永遠不會有值),而 unknown 只能賦值給 anyunknown 或在縮窄後的具體型別。


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('不是合法的使用者物件');
  }
}

常見陷阱與最佳實踐

陷阱 說明 最佳實踐
直接把 anyunknown 使用 失去編譯期安全檢查 避免 使用 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. 透過 assertNeverexhaustive 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);
    }
  }
}

總結

  • neverunknownTypeScript 型別系統 中的兩把「安全之劍」:never 用於 永不會返回 的情境,unknown 則是 安全的 any
  • 正確使用 never 可以 保證程式碼的不可達性,尤其在 exhaustive checking 時發揮關鍵作用。
  • unknown 必須 先縮窄,透過 typeofininstanceof自訂型別保護,才能安全地存取屬性或呼叫方法。
  • 在實務開發中,將 any 換成 unknown、搭配 assertNever、型別保護與完整性檢查,可顯著降低 runtime 錯誤、提升程式碼可維護性。

掌握這兩個型別的概念與最佳實踐,將讓你的 TypeScript 專案在 型別安全可讀性 兩方面同時升級,成為更可靠的程式碼基礎。祝開發順利!