本文 AI 產出,尚未審核

TypeScript 工具型別:NonNullable<T>

簡介

在日常開發中,我們常會遇到 nullundefined 這兩個特殊值。它們雖然在 JavaScript 中非常靈活,但在 TypeScript 的型別系統裡,若未妥善處理,會導致編譯錯誤或執行時的不可預期行為。
NonNullable<T> 正是為了解決這類問題而設計的 工具型別(Utility Type)。它能把傳入的型別 T 中的 nullundefined 移除,讓開發者在後續的程式碼裡不必再為這兩個值寫額外的防護。

對於 初學者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 包含 nullundefined,則回傳 never(代表「不存在」的型別),否則回傳原本的 T
  • 最終結果是 「原型別去除 nullundefined

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 同時包含 nullundefinedstring,使用 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]> 保證取得的屬性值永遠不會是 undefinednull,即使原型別中有這兩個可能。

範例 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:搭配 PartialRequired

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 voidundefined 在型別上不同,NonNullable<void> 仍會保留 void,導致意外的函式回傳型別 若要同時排除 void,可自行組合條件型別:type NotVoid<T> = T extends void ? never : T;
與聯集型別混用時的分布性 `NonNullable<string null
使用於函式回傳時忽略 never 若原型別全部都是 null/undefinedNonNullable<T> 會變成 never,呼叫者會得到「無法取得值」的錯誤 在設計 API 時,避免讓所有可能都被剔除,或在回傳前先做 default 處理
as 斷言混用過度 為了「強迫」型別通過,過度使用 as NonNullable<...> 會失去型別檢查的意義 儘量在資料來源(如 API、表單)就做好 型別守衛,只在確定安全時才使用斷言

最佳實踐

  1. 盡量在函式或介面的入口點使用 NonNullable,讓錯誤在呼叫端即被捕捉。
  2. 配合型別守衛(type guard),在需要時再細分 null/undefined 的處理邏輯。
  3. 避免在不必要的地方使用 as 斷言,保持型別系統的完整性。
  4. 在大型型別(如 API 回傳)上使用映射型別結合 NonNullable,一次性清理多層可能的 null/undefined
  5. 寫單元測試:即使型別系統已保證安全,仍建議測試實際的資料流,以防外部資料不符合預期。

實際應用場景

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 工具型別 中的基礎卻強大的成員,它能在 編譯階段nullundefined 徹底剔除,讓程式碼更安全、可讀性更高。透過本文的範例與實務情境,我們看到:

  • 核心行為NonNullable<T> 透過條件型別把 nullundefined 轉為 never,最終得到「純淨」的型別。
  • 常見陷阱:注意 voidnever 以及分布式條件型別的細節,避免不必要的型別斷言。
  • 最佳實踐:在 API 入口、泛型函式、狀態管理與表單驗證等關鍵點使用,配合型別守衛與映射型別,可一次性清理多層的可空型別。
  • 實務應用:從前端表單、API 回傳、全域狀態到函式庫開發,都能看到 NonNullable 的身影,幫助我們在 型別層面 先行捕獲錯誤,減少執行時的防呆成本。

掌握 NonNullable<T>,不僅能提升程式碼的 健全性,同時也能讓團隊在大型專案中更安心地使用 TypeScript 的型別系統。祝你在日常開發中玩得開心,寫出更安全、更可維護的程式!