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;
}
age與email在實例化時可以省略,若省略,TypeScript 會自動把它們的型別視為number | undefined與string | 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',
};
重點:未提供
publishedYear、isbn時,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,在使用時忘記同時檢查 undefined與null,容易產生 undefined` 錯誤。 |
| 可選屬性與索引簽名不相容 | 若索引簽名的型別過於嚴格,會限制可選屬性的型別,引發編譯錯誤。 | 確保索引簽名的型別包含所有可選屬性的可能型別(例如加入 undefined)。 |
| 在函式內直接解構可選屬性 | function f({a, b}: {a?: number, b?: string}) {} 直接解構會把 a、b 推斷為 必填,導致使用時出現 undefined 錯誤。 |
使用預設值或 = {} 來避免解構錯誤:function f({a, b}: {a?: number, b?: string} = {}) {}。 |
| 忘記提供預設值 | 呼叫接受可選參數的函式時,未提供參數,內部直接使用 options.prop 可能得到 undefined。 |
在函式內使用 null 合併運算子 (??) 或 預設參數 (options = {}) 來保護。 |
最佳實踐
- 明確標註可選:只要有業務需求允許缺少,務必加上
?,不要依賴any或unknown。 - 使用
Partial<T>進行批次可選:對於需要「全部可選」的情況(如更新 API),直接使用Partial<T>,保持型別一致性。 - 配合
??或?.:在存取可選屬性時,使用Nullish Coalescing (??) 或Optional Chaining (?.) 來避免undefined錯誤。 - 保持介面單一職責:不要把所有可能的屬性一次寫在同一個介面,必要時拆成多個介面或使用 交叉類型 (
&) 組合。 - 文件化:在介面註解中說明「此屬性為可選,若未提供會有什麼預設行為」或「何時會回傳
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; // 某些語系可能未翻譯
}
渲染時若 logout 為 undefined,則直接隱藏或使用 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 coalescing、optional chaining等語法,寫出更安全、可維護的程式碼。
在實務開發中,從 表單資料、API 回傳、套件設定 到 多語系資源,可選屬性都能幫助我們以最小的型別描述,涵蓋最大的彈性。只要留意常見的陷阱(如忘記檢查 undefined、與索引簽名衝突等),並遵守最佳實踐(明確標註、使用預設值、文件化),就能在 TypeScript 專案中充分發揮可選屬性的威力。
祝你在 TypeScript 的世界裡寫出既 安全 又 彈性 的程式碼! 🚀