TypeScript 工具型別:NonNullable<T>
簡介
在日常開發中,我們常會遇到 null 或 undefined 這兩個特殊值。它們雖然在 JavaScript 中非常靈活,但在 TypeScript 的型別系統裡,若未妥善處理,會導致編譯錯誤或執行時的不可預期行為。NonNullable<T> 正是為了解決這類問題而設計的 工具型別(Utility Type)。它能把傳入的型別 T 中的 null 與 undefined 移除,讓開發者在後續的程式碼裡不必再為這兩個值寫額外的防護。
對於 初學者,NonNullable<T> 可以幫助快速了解 TypeScript 如何透過型別操作提升程式安全性;對 中級開發者,則是打造更嚴謹 API、減少冗長檢查的利器。本文將從概念、範例、常見陷阱到實務應用,完整說明 NonNullable<T> 的使用方式與最佳實踐。
核心概念
1. NonNullable<T> 的語法與行為
type NonNullable<T> = T extends null | undefined ? never : T;
T extends null | undefined ? never : T:如果T包含null或undefined,則回傳never(代表「不存在」的型別),否則回傳原本的T。- 最終結果是 「原型別去除
null與undefined」。
註:
never在聯集型別(union)中會自動被過濾掉,例如string | never會簡化成string。
2. 為什麼需要 NonNullable?
| 情境 | 沒使用 NonNullable |
使用 NonNullable |
|---|---|---|
| 函式參數 | function foo(v: string | null) {}使用時必須每次檢查 v !== null |
`function foo(v: NonNullable<string |
| 物件屬性 | type User = { name?: string }取值時仍可能是 undefined |
`type User = { name: NonNullable<string |
| 泛型 | function get<T>(key: keyof T) { return obj[key]; }回傳可能是 `T[keyof T] |
undefined` |
透過 NonNullable,我們可以 在型別層面 直接排除 null/undefined,減少執行時的防呆程式碼,讓程式更清晰、錯誤更早被捕獲。
3. 基礎範例
範例 1:簡單型別去除
type A = string | null | undefined;
type B = NonNullable<A>; // B 為 string
說明:
A同時包含null、undefined與string,使用NonNullable後只剩下string。
範例 2:函式參數的安全性
function greet(name: NonNullable<string | null>) {
// name 在此一定是 string,編譯器不允許傳入 null
console.log(`Hello, ${name}!`);
}
// 正確使用
greet('Alice');
// 錯誤示範:編譯時會報錯
// greet(null); // Argument of type 'null' is not assignable to parameter of type 'string'.
重點:
NonNullable直接把null排除,使得函式的使用者在呼叫時就能得到即時的型別錯誤提示。
範例 3:結合泛型與條件型別
function getValue<T, K extends keyof T>(obj: T, key: K): NonNullable<T[K]> {
return obj[key] as NonNullable<T[K]>;
}
type Config = {
host: string | undefined;
port: number | null;
};
const cfg: Config = { host: 'localhost', port: 8080 };
const host = getValue(cfg, 'host'); // host 為 string
const port = getValue(cfg, 'port'); // port 為 number
說明:
getValue透過NonNullable<T[K]>保證取得的屬性值永遠不會是undefined或null,即使原型別中有這兩個可能。
範例 4:在介面中使用
interface ApiResponse<T> {
data: T | null;
error?: string;
}
// 取得必定有資料的回傳型別
type SuccessResponse<T> = {
data: NonNullable<T>;
error?: never;
};
type User = { id: number; name: string };
type SafeUserResponse = SuccessResponse<ApiResponse<User>>;
// SafeUserResponse 等同於
// {
// data: { id: number; name: string };
// error?: never;
// }
技巧:把
NonNullable放在介面或型別別名裡,可以一次性「淨化」多層的null/undefined。
範例 5:搭配 Partial 與 Required
type PartialUser = Partial<{ id: number; name: string; email: string }>;
type CompleteUser = Required<PartialUser>; // 所有屬性皆必填
// 假設某些屬性允許 null,但最終一定要被清除
type RawUser = {
id: number;
name: string | null;
email?: string | undefined;
};
type CleanUser = {
[K in keyof RawUser]-?: NonNullable<RawUser[K]>;
};
// CleanUser => { id: number; name: string; email: string }
說明:使用映射型別 (
[K in keyof RawUser]) 搭配-?(移除可選)與NonNullable,一次完成 「必填」+「去除 null/undefined」 的轉換。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方式 |
|---|---|---|
誤把 void 當成 undefined |
void 與 undefined 在型別上不同,NonNullable<void> 仍會保留 void,導致意外的函式回傳型別 |
若要同時排除 void,可自行組合條件型別:type NotVoid<T> = T extends void ? never : T; |
| 與聯集型別混用時的分布性 | `NonNullable<string | null |
使用於函式回傳時忽略 never |
若原型別全部都是 null/undefined,NonNullable<T> 會變成 never,呼叫者會得到「無法取得值」的錯誤 |
在設計 API 時,避免讓所有可能都被剔除,或在回傳前先做 default 處理 |
與 as 斷言混用過度 |
為了「強迫」型別通過,過度使用 as NonNullable<...> 會失去型別檢查的意義 |
儘量在資料來源(如 API、表單)就做好 型別守衛,只在確定安全時才使用斷言 |
最佳實踐
- 盡量在函式或介面的入口點使用
NonNullable,讓錯誤在呼叫端即被捕捉。 - 配合型別守衛(type guard),在需要時再細分
null/undefined的處理邏輯。 - 避免在不必要的地方使用
as斷言,保持型別系統的完整性。 - 在大型型別(如 API 回傳)上使用映射型別結合
NonNullable,一次性清理多層可能的null/undefined。 - 寫單元測試:即使型別系統已保證安全,仍建議測試實際的資料流,以防外部資料不符合預期。
實際應用場景
1. 前端表單驗證
在表單送出前,我們會先把使用者輸入的值轉換成 必定非 null/undefined 的型別,避免後端 API 收到缺失欄位。
type RawForm = {
username: string | null;
age?: number | undefined;
};
type CleanForm = {
[K in keyof RawForm]-?: NonNullable<RawForm[K]>;
};
function submit(form: CleanForm) {
// 這裡的 form 已保證每個欄位都有值
api.post('/users', form);
}
2. 服務端 API 回傳處理
後端有時會回傳 null 表示「未找到」或「尚未設定」的欄位。前端在收到回傳後,可立即使用 NonNullable 轉型,讓後續的渲染邏輯更簡潔。
type ApiUser = {
id: number;
name: string | null;
avatarUrl?: string | null;
};
async function fetchUser(id: number): Promise<NonNullable<ApiUser>> {
const res = await fetch(`/api/users/${id}`);
const data: ApiUser = await res.json();
// 直接斷言為非 null,前提是我們已在服務端保證
return data as NonNullable<ApiUser>;
}
3. Redux / Zustand 狀態管理
在全域狀態中,常會把某些屬性設為 null 作為「未載入」的標記。使用 NonNullable 可以在 selector 中保證取得的值一定已經初始化。
interface RootState {
profile: {
user: User | null;
loading: boolean;
};
}
// selector
const selectCurrentUser = (state: RootState): NonNullable<User> => {
if (!state.profile.user) {
throw new Error('User not loaded');
}
return state.profile.user;
};
4. 泛型函式庫(如 lodash、ramda)型別強化
在自訂的工具函式庫中,常會提供 「保證非空」 的 API,例如 compact<T>(arr: (T | null | undefined)[]) => NonNullable<T>[]。
function compact<T>(arr: (T | null | undefined)[]): NonNullable<T>[] {
return arr.filter((v): v is NonNullable<T> => v != null);
}
// 使用
const mixed = [1, null, 2, undefined, 3];
const numbers = compact(mixed); // numbers 為 number[]
總結
NonNullable<T> 是 TypeScript 工具型別 中的基礎卻強大的成員,它能在 編譯階段 把 null 與 undefined 徹底剔除,讓程式碼更安全、可讀性更高。透過本文的範例與實務情境,我們看到:
- 核心行為:
NonNullable<T>透過條件型別把null、undefined轉為never,最終得到「純淨」的型別。 - 常見陷阱:注意
void、never以及分布式條件型別的細節,避免不必要的型別斷言。 - 最佳實踐:在 API 入口、泛型函式、狀態管理與表單驗證等關鍵點使用,配合型別守衛與映射型別,可一次性清理多層的可空型別。
- 實務應用:從前端表單、API 回傳、全域狀態到函式庫開發,都能看到
NonNullable的身影,幫助我們在 型別層面 先行捕獲錯誤,減少執行時的防呆成本。
掌握 NonNullable<T>,不僅能提升程式碼的 健全性,同時也能讓團隊在大型專案中更安心地使用 TypeScript 的型別系統。祝你在日常開發中玩得開心,寫出更安全、更可維護的程式!