本文 AI 產出,尚未審核

TypeScript 基本型別:nullundefinedvoidnever


簡介

在 TypeScript 中,型別安全是最核心的價值之一,而「基本型別」是建構安全程式的根基。
nullundefinedvoidnever 雖然看起來只是幾個特殊值,卻在 錯誤處理、函式簽名、型別推斷 等多個層面扮演關鍵角色。

如果對這四種型別的行為與使用時機不夠清楚,常會出現「型別不相容」或「執行時意外」的問題。
本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現它們在實務開發中的應用,幫助你從 初學者 逐步升級為 中級 的 TypeScript 使用者。


核心概念

1. nullundefined 的差別

型別 代表的意義 預設值 常見出現情境
null 明確的空值,表示「此處本應有值,但目前沒有」 變數若未賦值不會自動得到 null API 回傳值缺失、資料庫查無結果
undefined 未定義,表示「變數尚未被初始化」或「函式沒有回傳值」 所有未賦值的變數、函式參數預設為 undefined 可選參數、未實作的屬性

為什麼要區分?

  • 語意清晰null 用於「有意的空」;undefined 用於「未被賦值」。
  • 型別檢查:在 strictNullChecks 開啟時,兩者不會自動相容,能提前捕捉錯誤。
// 範例 1:strictNullChecks 開啟時的型別衝突
function getLength(s: string | null): number {
  // if (s === null) return 0; // 必須先處理 null
  return s.length; // ❌ 直接存取會報錯
}

2. void:函式不回傳值

void 表示 沒有任何可用的回傳值,常用於副作用函式(例如寫入日志、觸發 UI 更新)。
strictNullChecks 開啟時,void 只能接受 undefined,但在允許隱式的 null 時,null 也會被視為合法。

// 範例 2:void 函式
function log(message: string): void {
  console.log(message);
  // return; // 可省略,預設回傳 undefined
}
const result: void = log('Hello'); // result 的值為 undefined

3. never:永遠不會有返回值

never 用來描述 不會正常結束 的情況,例如:

  • 拋出例外 (throw)
  • 無限迴圈 (while (true) {})

它是 最底層 的型別,所有其他型別都可以賦值給 never,但 never 只能賦值給任何型別(因為它根本不存在值)。

// 範例 3:never 用於錯誤處理
function fail(message: string): never {
  throw new Error(message);
}

// 使用在條件分支中,確保所有情況都有回傳
function getStatus(code: 0 | 1 | 2): string {
  switch (code) {
    case 0: return '成功';
    case 1: return '失敗';
    case 2: return '未知';
    default:
      return fail('不可能的代碼'); // 這裡的返回型別被推斷為 never
  }
}

4. 交叉使用:null | undefinedvoid | undefined

在實務開發中,常會看到以下組合型別:

type Maybe<T> = T | null | undefined;
type Callback = (() => void) | undefined;
  • Maybe<T> 表示 可選值,適合 API 回傳或表單欄位。
  • Callback 表示 可選的回呼函式,若未提供則為 undefined

程式碼範例

範例 1:安全存取可能為 nullundefined 的屬性

interface User {
  id: number;
  name?: string; // name 可能是 undefined
  address: string | null; // address 可能是 null
}

function greet(user: User): string {
  // 使用可選鏈 (?.) + 空值合併 (??) 讓程式更簡潔
  const name = user.name ?? '匿名';
  const address = user.address ?? '未知地點';
  return `嗨,${name}!您的地址是 ${address}。`;
}

範例 2:voidPromise<void> 的差異

// 普通的 void 函式
function saveLog(msg: string): void {
  console.log('Log:', msg);
}

// 非同步的 void,回傳 Promise<void>
async function saveLogAsync(msg: string): Promise<void> {
  await fetch('/api/log', { method: 'POST', body: JSON.stringify({ msg }) });
}

// 使用時的型別提示
const r1: void = saveLog('test');          // r1 為 undefined
const r2: Promise<void> = saveLogAsync('test'); // r2 為 Promise,需 await

範例 3:never 在自訂類型守衛中的應用

type Shape = 
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2;
    case 'square': return s.side ** 2;
    default:
      // 透過 never 讓編譯器檢查所有可能性
      const _exhaustiveCheck: never = s;
      return _exhaustiveCheck;
  }
}

範例 4:函式重載與 voidnever 的結合

function parse(json: string): any;               // 正常回傳
function parse(json: string, reviver: never): any; // 不允許第二個參數
function parse(json: string, reviver?: any): any {
  if (reviver !== undefined) {
    // 這裡的 reviver 會被視為 never,編譯期會報錯
    throw new Error('reviver 參數已棄用');
  }
  return JSON.parse(json);
}

範例 5:使用 strictNullChecks 防止意外的 null/undefined

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,               // 包含 strictNullChecks
    "strictNullChecks": true
  }
}

// 下面的程式碼在 strict 模式下會警告
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0]; // 若陣列為空,回傳 undefined
}

const name: string = getFirst(['Alice', 'Bob']); // ❌ Type 'string | undefined' is not assignable

常見陷阱與最佳實踐

陷阱 說明 解決方式
nullundefined strictNullChecks 關閉時,兩者會自動相容,導致隱藏的 null 錯誤。 開啟 strictNullChecks,明確區分 nullundefined
void 函式不小心回傳值 有時候不小心在 void 函式內寫了 return something;,會讓編譯器產生隱式的 any 避免在 void 函式中使用 return,或在返回時寫 return;
never 被誤用於普通函式 never 用於可能正常結束的函式,會導致呼叫端無法得到正確型別。 僅在確定不會回傳(拋例外、無限迴圈)時使用 never
可選參數的預設值忘記處理 undefined 呼叫者不傳參數時,參數會是 undefined,若直接使用會拋錯。 使用 預設參數 (function foo(x: number = 0)) 或 空值合併 (x ?? default)。
過度使用 any 逃避 null/undefined 為了省事直接宣告 any,失去型別保護。 利用 unknown泛型 來保留型別資訊,同時在需要時做 型別斷言

最佳實踐小結

  1. 始終開啟 strict(特別是 strictNullChecks,讓編譯器在開發階段捕捉 null/undefined 的問題。
  2. 使用 可選鏈 (?.)空值合併 (??) 取代繁雜的 if 判斷,提高可讀性。
  3. 函式若沒有回傳值,明確標註為 void;若一定會拋錯或永不返回,使用 never
  4. API 回傳或表單欄位 建立 Maybe<T> 類型,統一管理「可能缺值」的情況。
  5. 型別守衛條件分支 中加入 never 的「窮盡檢查」(exhaustive check),確保未來新增型別時會被編譯器提醒。

實際應用場景

1. 前端表單驗證

type FormValue = string | null | undefined;

function validateEmail(value: FormValue): boolean {
  if (value == null) return false; // 同時檢查 null 與 undefined
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(value);
}
  • 目的:同時處理使用者未填寫 (undefined) 與清除欄位 (null) 的情況,避免 value.length 引發例外。

2. 後端 API 回傳的可選屬性

interface UserResponse {
  id: number;
  name: string;
  avatarUrl?: string | null; // 前端可接受 null 或未提供
}
  • 目的:使用 string | null 讓前端在渲染時能區分「沒有設定」與「設定為空」。

3. 中介層(Middleware)中的 void

type Middleware = (req: Request, res: Response, next: () => void) => void;

const logger: Middleware = (req, _res, next) => {
  console.log(`${req.method} ${req.url}`);
  next(); // next 返回 void
};
  • 目的:明確表達 next 沒有回傳值,避免誤以為它會返回結果。

4. 全局錯誤處理器使用 never

function handleError(e: unknown): never {
  if (e instanceof Error) {
    console.error(e.message);
  } else {
    console.error('未知錯誤');
  }
  process.exit(1); // 程式結束,永不返回
}
  • 目的:告訴 TypeScript 此函式結束後程式不會繼續執行,讓後續程式碼的型別推斷更精確。

總結

  • nullundefined 雖然都代表「沒有值」,但語意不同;在 strictNullChecks 開啟時必須分別處理。
  • void 用於「沒有實際回傳值」的函式,常見於副作用或回呼函式;配合 Promise<void> 可表達非同步的「無返回」情境。
  • never 描述「永不返回」的情況,適合錯誤拋出、無限迴圈或型別守衛的窮盡檢查,能提升程式的型別安全性。
  • 透過 可選鏈 (?.)空值合併 (??)泛型 Maybe<T> 等技巧,我們可以在實務開發中更優雅地處理可能缺值的情況。
  • 開啟 strict 編譯選項明確標註 void/never避免濫用 any,是避免常見陷阱的關鍵。

掌握這四種基本型別的正確使用方式,不僅能讓程式碼更健全易於維護,也能在大型專案中減少因型別不一致而產生的錯誤。祝你在 TypeScript 的旅程中,寫出更安全、更可讀的程式!