本文 AI 產出,尚未審核

TypeScript 物件與介面(Objects & Interfaces)— Optional 屬性(?)


簡介

在日常的前端或 Node.js 專案中,我們常常需要描述「某個屬性可能會出現,也可能不會出現」的資料結構。若直接把屬性寫成必填,則在缺少該屬性時 TypeScript 會拋出型別錯誤,開發者必須額外寫大量的 if (obj.prop !== undefined) 判斷,程式碼變得雜亂。

Optional(可選)屬性則提供了一種 宣告層面的彈性:開發者可以在介面或型別定義時,明確指出哪些屬性是「可有可無」的。這不僅提升了程式碼的可讀性,也讓編譯器在檢查時能正確推斷出屬性的可能型別(T | undefined),避免潛在的執行時錯誤。

本篇文章將從概念、語法到實務應用,完整說明在 TypeScript 中使用 optional 屬性 的方式與注意事項,讓你在撰寫介面時既安全又靈活。


核心概念

1. 基本語法:在屬性名稱後加 ?

在介面或型別別名中,只要在屬性名稱的最後加上問號 (?),即可將該屬性標記為可選。

interface UserProfile {
  /** 使用者名稱,必填 */
  name: string;
  /** 年齡可選,若未提供則為 undefined */
  age?: number;
  /** 電子郵件可選 */
  email?: string;
}
  • ageemail 在實例化時可以省略,若省略,TypeScript 會自動把它們的型別視為 number | undefinedstring | undefined

2. 可選屬性與 Partial<T> 的關係

Partial<T> 是 TypeScript 標準庫提供的泛型工具型別,會把 所有 屬性都變成可選。它的實作其實就是把每個屬性加上 ?

type PartialUser = Partial<UserProfile>;
/*
等同於:
type PartialUser = {
  name?: string;
  age?: number;
  email?: string;
}
*/

小技巧:當你只想在某些情況下允許部份更新(例如 PATCH API),可以直接使用 Partial<T>,省去手動寫 ? 的麻煩。

3. 可選屬性與索引簽名(Index Signature)的互動

如果介面同時使用了可選屬性與索引簽名([key: string]: any),TypeScript 會把可選屬性視為 必須符合索引簽名的型別,否則會產生錯誤。

interface Config {
  /** 必填屬性 */
  mode: 'development' | 'production';
  /** 可選屬性 */
  debug?: boolean;
  /** 任意額外屬性 */
  [key: string]: string | boolean | undefined;
}

此例中,debug 必須是 boolean | undefined,否則會與索引簽名衝突。

4. 可選屬性與函式參數的差異

在函式參數中使用 ? 與在介面中使用 ? 的行為相似,但 函式參數的可選性還會影響函式呼叫的簽名(overload resolution)。

function greet(name: string, title?: string) {
  console.log(`Hello, ${title ? title + ' ' : ''}${name}!`);
}

// 呼叫時可以省略 title
greet('Alice');          // Hello, Alice!
greet('Bob', 'Dr.');     // Hello, Dr. Bob!

5. 可選屬性與 null 的區別

? 只代表「可能不存在」 (undefined);如果你希望屬性同時接受 null,必須在型別中顯式加入 null

interface Product {
  /** 可選且允許 null */
  description?: string | null;
}

程式碼範例

以下示範 5 個常見的實務情境,說明如何正確使用 optional 屬性。

範例 1:建立具備可選屬性的介面並建立實例

interface Book {
  title: string;          // 必填
  author: string;         // 必填
  publishedYear?: number; // 可選
  isbn?: string;          // 可選
}

// 只提供必填屬性
const novel: Book = {
  title: '追風箏的人',
  author: '卡勒德·胡賽尼',
};

// 同時提供可選屬性
const textbook: Book = {
  title: '深入淺出 TypeScript',
  author: '張三',
  publishedYear: 2023,
  isbn: '978-986-123456-7',
};

重點:未提供 publishedYearisbn 時,novel 仍符合 Book 型別,因為它們被標記為 optional

範例 2:利用 Partial<T> 進行資料更新(PATCH)

interface User {
  id: number;
  username: string;
  email: string;
  role?: 'admin' | 'user';
}

// 假設收到前端的 PATCH 請求,只包含要變更的欄位
function updateUser(id: number, updates: Partial<User>) {
  // 這裡會把 updates 合併到資料庫中的舊資料
  // 省略實作細節
  console.log(`Updating user ${id} with`, updates);
}

// 呼叫時只傳入想修改的欄位
updateUser(1, { email: 'new@email.com' });
updateUser(2, { role: 'admin', username: 'newName' });

技巧Partial<User> 讓所有屬性自動變為可選,減少重複宣告。

範例 3:配合索引簽名儲存額外設定

interface ThemeConfig {
  /** 佈景主題名稱,必填 */
  name: string;
  /** 是否啟用暗色模式,可選 */
  darkMode?: boolean;
  /** 任意額外的設定鍵值 */
  [key: string]: string | boolean | undefined;
}

// 只提供基本設定
const basic: ThemeConfig = {
  name: 'light',
};

// 加入自訂屬性
const custom: ThemeConfig = {
  name: 'custom',
  darkMode: true,
  primaryColor: '#ff6600',   // 額外屬性
  showSidebar: false,
};

注意:額外屬性的型別必須符合索引簽名的定義,否則會編譯錯誤。

範例 4:在函式參數中使用可選屬性

interface LogOptions {
  /** 記錄等級,預設為 'info' */
  level?: 'debug' | 'info' | 'warn' | 'error';
  /** 是否顯示時間戳記 */
  timestamp?: boolean;
}

/**
 * 輸出日誌訊息
 */
function logger(message: string, options: LogOptions = {}) {
  const level = options.level ?? 'info';
  const time = options.timestamp ? new Date().toISOString() + ' ' : '';
  console.log(`[${level}] ${time}${message}`);
}

// 使用預設值
logger('系統啟動');

// 指定部分屬性
logger('使用者登入失敗', { level: 'warn' });
logger('資料已儲存', { timestamp: true });

關鍵:透過 options: LogOptions = {} 提供預設空物件,讓呼叫者可以完全省略 options

範例 5:可選屬性與 null 的結合

interface ApiResponse<T> {
  /** 成功時返回的資料 */
  data?: T | null;
  /** 錯誤訊息,若成功則為 undefined */
  error?: string;
}

// 成功且有資料
const success1: ApiResponse<number> = { data: 42 };

// 成功但資料為 null(代表空結果)
const success2: ApiResponse<number> = { data: null };

// 失敗的回應
const failure: ApiResponse<number> = { error: 'Invalid request' };

說明data?: T | null 同時允許 缺少屬性 (undefined) 與 明確的 null,適用於 API 回傳「無資料」與「錯誤」的情境。


常見陷阱與最佳實踐

陷阱 說明 解決方式
把可選屬性寫成必填 忘記加 ?,導致在建立物件時必須提供所有欄位,增加維護成本。 建立介面時先列出所有欄位,再逐一評估哪個真的可以省略,最後加上 ?
可選屬性與 null 混用 直接寫 `prop?: string null,在使用時忘記同時檢查 undefinednull,容易產生 undefined` 錯誤。
可選屬性與索引簽名不相容 若索引簽名的型別過於嚴格,會限制可選屬性的型別,引發編譯錯誤。 確保索引簽名的型別包含所有可選屬性的可能型別(例如加入 undefined)。
在函式內直接解構可選屬性 function f({a, b}: {a?: number, b?: string}) {} 直接解構會把 ab 推斷為 必填,導致使用時出現 undefined 錯誤。 使用預設值或 = {} 來避免解構錯誤:function f({a, b}: {a?: number, b?: string} = {}) {}
忘記提供預設值 呼叫接受可選參數的函式時,未提供參數,內部直接使用 options.prop 可能得到 undefined 在函式內使用 null 合併運算子 (??) 或 預設參數 (options = {}) 來保護。

最佳實踐

  1. 明確標註可選:只要有業務需求允許缺少,務必加上 ?,不要依賴 anyunknown
  2. 使用 Partial<T> 進行批次可選:對於需要「全部可選」的情況(如更新 API),直接使用 Partial<T>,保持型別一致性。
  3. 配合 ???.:在存取可選屬性時,使用Nullish Coalescing (??) 或Optional Chaining (?.) 來避免 undefined 錯誤。
  4. 保持介面單一職責:不要把所有可能的屬性一次寫在同一個介面,必要時拆成多個介面或使用 交叉類型 (&) 組合。
  5. 文件化:在介面註解中說明「此屬性為可選,若未提供會有什麼預設行為」或「何時會回傳 null」,提升團隊可讀性。

實際應用場景

1. 前端表單的動態欄位

在建立使用者設定表單時,某些欄位只有在特定選項開啟時才會顯示。介面可用 optional 屬性描述:

interface SettingsForm {
  theme: 'light' | 'dark';
  /** 當 theme 為 'dark' 時才需要 */
  darkModeIntensity?: number;
}

表單提交時,只需要傳送實際出現的欄位,後端可直接使用 Partial<SettingsForm> 來驗證。

2. API 回傳的分頁結果

分頁 API 常會回傳 nextPageToken,但在最後一頁時此欄位會缺失:

interface PageResult<T> {
  items: T[];
  /** 若有下一頁則提供 token,否則為 undefined */
  nextPageToken?: string;
}

前端只要檢查 result.nextPageToken?.length 即可決定是否載入更多。

3. 多語系資源檔

不同語系可能缺少某些翻譯鍵值,使用 optional 屬性可以允許缺失:

interface LocaleStrings {
  welcome: string;
  logout?: string; // 某些語系可能未翻譯
}

渲染時若 logoutundefined,則直接隱藏或使用 fallback。

4. 第三方套件的設定物件

許多 npm 套件提供一個「設定物件」給使用者,常見的做法是把不常用的選項設為 optional,讓使用者只需設定必要的部分:

interface ChartOptions {
  type: 'line' | 'bar' | 'pie';
  /** 是否顯示圖例,預設 true */
  legend?: boolean;
  /** 圖表動畫設定,較少使用 */
  animation?: {
    duration?: number;
    easing?: string;
  };
}

開發者只需傳入 { type: 'line' },其餘項目會採用套件內建的預設值。


總結

Optional 屬性 是 TypeScript 介面設計中不可或缺的工具,透過在屬性名稱後加上 ?,我們可以:

  • 明確表達「此欄位可能不存在」的業務需求。
  • 讓編譯器自動將型別擴充為 T | undefined,減少手動檢查的負擔。
  • 搭配 Partial<T>nullish coalescingoptional chaining 等語法,寫出更安全、可維護的程式碼。

在實務開發中,從 表單資料API 回傳套件設定多語系資源,可選屬性都能幫助我們以最小的型別描述,涵蓋最大的彈性。只要留意常見的陷阱(如忘記檢查 undefined、與索引簽名衝突等),並遵守最佳實踐(明確標註、使用預設值、文件化),就能在 TypeScript 專案中充分發揮可選屬性的威力。

祝你在 TypeScript 的世界裡寫出既 安全彈性 的程式碼! 🚀