TypeScript 工具型別:Required<T>
簡介
在大型的 TypeScript 專案中,我們常常會使用 介面(interface) 或 型別別名(type alias) 來描述物件的結構。為了提升彈性,許多屬性會被設計成可選 (?) 或是使用 Partial<T> 產生的全部可選版本。
然而在實際執行時,某些情境卻需要保證所有屬性皆為必填,此時 Required<T> 這個工具型別就派上用場了。它能在不改變原始介面的前提下,快速把所有可選屬性轉為必填,讓編譯器在型別檢查時提供更嚴格的保證。
本篇文章將深入說明 Required<T> 的原理、使用方式、常見陷阱與最佳實踐,並提供多個實務範例,幫助你在日常開發中靈活運用這個工具型別。
核心概念
為什麼需要 Required<T>?
- 資料完整性:在 API 回傳或表單送出前,需要確保所有欄位都有值,避免因遺漏而產生錯誤。
- 型別安全:將可選屬性強制為必填,可讓編譯器在開發階段即捕捉到未賦值的情況。
- 重用性:不需要為每個情境重新撰寫完整的介面,只要在需要的地方套用
Required<T>即可。
語法與行為
Required<T> 是 TypeScript 內建的 工具型別(Utility Types),其宣告大致如下(簡化版):
type Required<T> = {
[P in keyof T]-?: T[P];
};
keyof T取得T的所有屬性鍵名。-?移除屬性上的可選修飾 (?);若屬性本身已是必填,則不受影響。- 最終回傳一個新型別,所有屬性皆為必填。
注意:
Required<T>只會改變屬性的「必選」狀態,不會改變屬性的原始型別(例如string | undefined仍會保留undefined)。
範例一:最基本的使用
interface User {
id: number;
name?: string; // 可選屬性
email?: string; // 可選屬性
}
// 直接套用 Required
type CompleteUser = Required<User>;
const u1: CompleteUser = {
id: 1,
name: "Alice",
email: "alice@example.com", // 必填,缺少會編譯錯誤
};
CompleteUser內的name、
範例二:與 Partial<T> 結合
有時我們會先使用 Partial<T> 讓所有屬性變為可選,再根據條件把部份屬性變回必填:
type UpdateUser = Required<Partial<User>>;
// 等同於把所有屬性全部必填(其實就是 User 本身的必填版)
const u2: UpdateUser = {
id: 2,
name: "Bob",
email: "bob@example.com",
};
雖然此例看起來多此一舉,但在 泛型函式 中會非常實用,請看下一個範例。
範例三:泛型函式中動態切換必填屬性
function save<T>(data: T): Required<T> {
// 假設在執行前已經完成所有欄位的填充
return data as Required<T>;
}
// 使用者輸入的資料可能只填了一部分
const partial: Partial<User> = { id: 3, name: "Carol" };
const complete = save(partial); // complete 的型別為 Required<User>
console.log(complete.email); // 編譯器認為此屬性一定存在
在這個例子中,save 函式接受任意型別 T,回傳值會被視為 所有屬性皆已填充,因此後續的程式碼可以安全地存取每個欄位。
範例四:與映射型別(Mapped Types)混合使用
type ApiResponse<T> = {
data: Required<T>;
error?: string;
};
interface Product {
sku: string;
price?: number;
}
// API 回傳的 product 必定包含 price
type ProductResponse = ApiResponse<Product>;
const resp: ProductResponse = {
data: {
sku: "A001",
price: 1999, // 必填
},
};
ApiResponse 把 data 部分強制為必填,讓呼叫端不必再檢查 price 是否為 undefined。
範例五:深層必填(遞迴)— 自訂 DeepRequired
Required<T> 只會處理第一層屬性,若屬性本身是物件,內部的可選屬性仍會保留。下面示範如何自行實作 深層必填:
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
interface Order {
id: number;
customer?: {
name?: string;
address?: {
city?: string;
};
};
}
type FullOrder = DeepRequired<Order>;
const o: FullOrder = {
id: 100,
customer: {
name: "Dave",
address: {
city: "Taipei",
},
},
};
透過遞迴映射,我們把 所有層級 的可選屬性全部變為必填,這在處理深層資料結構(例如從後端取得的 JSON)時非常有用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
屬性型別仍保留 undefined |
Required<T> 只移除 ?,不會自動把 T[P] 中的 undefined 移除。若原本的型別是 string | undefined,仍需自行處理。 |
使用 自訂型別(如 NonNullable<T[P]>)或結合 Exclude<T[P], undefined>。 |
| 深層結構不會被遞迴 | 如前所述,Required<T> 只作用於第一層。 |
實作 DeepRequired<T> 或使用第三方套件(如 type-fest 的 PartialDeep、RequiredDeep)。 |
| 與交叉類型(Intersection)衝突 | 若同時使用 Required<T> 與其他交叉型別,可能產生意外的屬性合併結果。 |
盡量先 簡化型別,或使用 & 之後再套用 Required<>。 |
| 與映射型別的可選屬性互相抵消 | 在自訂映射型別中,-? 可能被再次加上 ?,導致屬性仍為可選。 |
注意 屬性修飾的順序,確保 -? 最後執行。 |
最佳實踐
- 只在需要「完整」資料的地方使用:例如 API 回傳、表單送出前的驗證。避免在全部程式碼中濫用,會失去可選屬性帶來的彈性。
- 結合
as const與Required<>:在常量物件上使用as const,再套用Required<>,可以得到 不可變且必填 的型別。 - 使用自訂深層必填:對於多層嵌套的資料,建立
DeepRequired<T>(或使用type-fest)以確保所有層級都被檢查。 - 保持型別的可讀性:若
Required<T>讓型別變得過於龐大,可考慮 拆分子型別,只對關鍵部分套用必填。
實際應用場景
表單驗證
- 使用者在 UI 上只填寫部分欄位,提交前透過函式把資料轉為
Required<T>,確保所有欄位都有值再送出。
- 使用者在 UI 上只填寫部分欄位,提交前透過函式把資料轉為
API 客戶端
- 從後端取得的資料可能缺少某些欄位(因為後端回傳的是
Partial<T>),在資料處理層先轉為Required<T>,讓後續的業務邏輯可以安全存取。
- 從後端取得的資料可能缺少某些欄位(因為後端回傳的是
測試資料建構
- 在單元測試中,我們常需要建立完整的測試物件。使用
Required<T>可以快速從最小的樣本資料產生完整物件,減少手動填寫的工作量。
- 在單元測試中,我們常需要建立完整的測試物件。使用
函式返回值保證
- 某些函式會根據條件補全缺失欄位,回傳值可宣告為
Required<T>,讓呼叫端不必再做額外的null/undefined檢查。
- 某些函式會根據條件補全缺失欄位,回傳值可宣告為
第三方套件的型別改寫
- 某些套件提供的型別是部分可選的,若你在自己的程式碼中需要全部必填,直接使用
Required<ThirdPartyType>即可,避免自行複製介面。
- 某些套件提供的型別是部分可選的,若你在自己的程式碼中需要全部必填,直接使用
總結
Required<T> 是 TypeScript 提供的 簡潔且強大的工具型別,讓開發者可以在不改動原始介面的情況下,快速把所有可選屬性變為必填。透過本文的說明與範例,你應該已經了解:
Required<T>的基本語法與底層原理- 如何在 泛型函式、API 回傳、表單驗證 等情境中靈活運用
- 常見的陷阱(如
undefined型別仍在)與解決方案(自訂DeepRequired、NonNullable等) - 最佳實踐:只在需要完整資料的地方使用、保持型別可讀性、結合深層必填技巧
掌握 Required<T> 後,你的 TypeScript 程式碼將更具型別安全、可維護性,也能在開發過程中更早捕捉到遺漏的欄位,提升整體開發效率。祝你在 TypeScript 的旅程中玩得開心、寫得順手!