本文 AI 產出,尚未審核
TypeScript 基本型別:null、undefined、void 與 never
簡介
在 TypeScript 中,型別安全是最核心的價值之一,而「基本型別」是建構安全程式的根基。null、undefined、void 與 never 雖然看起來只是幾個特殊值,卻在 錯誤處理、函式簽名、型別推斷 等多個層面扮演關鍵角色。
如果對這四種型別的行為與使用時機不夠清楚,常會出現「型別不相容」或「執行時意外」的問題。
本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現它們在實務開發中的應用,幫助你從 初學者 逐步升級為 中級 的 TypeScript 使用者。
核心概念
1. null 與 undefined 的差別
| 型別 | 代表的意義 | 預設值 | 常見出現情境 |
|---|---|---|---|
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 | undefined、void | undefined
在實務開發中,常會看到以下組合型別:
type Maybe<T> = T | null | undefined;
type Callback = (() => void) | undefined;
Maybe<T>表示 可選值,適合 API 回傳或表單欄位。Callback表示 可選的回呼函式,若未提供則為undefined。
程式碼範例
範例 1:安全存取可能為 null 或 undefined 的屬性
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:void 與 Promise<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:函式重載與 void、never 的結合
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
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
把 null 當 undefined 用 |
在 strictNullChecks 關閉時,兩者會自動相容,導致隱藏的 null 錯誤。 |
開啟 strictNullChecks,明確區分 null 與 undefined。 |
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 或 泛型 來保留型別資訊,同時在需要時做 型別斷言。 |
最佳實踐小結
- 始終開啟
strict(特別是strictNullChecks),讓編譯器在開發階段捕捉null/undefined的問題。 - 使用 可選鏈 (
?.) 與 空值合併 (??) 取代繁雜的if判斷,提高可讀性。 - 函式若沒有回傳值,明確標註為
void;若一定會拋錯或永不返回,使用never。 - 為 API 回傳或表單欄位 建立
Maybe<T>類型,統一管理「可能缺值」的情況。 - 在 型別守衛、條件分支 中加入
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 此函式結束後程式不會繼續執行,讓後續程式碼的型別推斷更精確。
總結
null與undefined雖然都代表「沒有值」,但語意不同;在strictNullChecks開啟時必須分別處理。void用於「沒有實際回傳值」的函式,常見於副作用或回呼函式;配合Promise<void>可表達非同步的「無返回」情境。never描述「永不返回」的情況,適合錯誤拋出、無限迴圈或型別守衛的窮盡檢查,能提升程式的型別安全性。- 透過 可選鏈 (
?.)、空值合併 (??)、泛型Maybe<T>等技巧,我們可以在實務開發中更優雅地處理可能缺值的情況。 - 開啟
strict編譯選項、明確標註void/never、避免濫用any,是避免常見陷阱的關鍵。
掌握這四種基本型別的正確使用方式,不僅能讓程式碼更健全、易於維護,也能在大型專案中減少因型別不一致而產生的錯誤。祝你在 TypeScript 的旅程中,寫出更安全、更可讀的程式!