本文 AI 產出,尚未審核

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 內的 nameemail 已被轉為必填,若省略任一欄位 TypeScript 會立刻報錯。

範例二:與 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, // 必填
  },
};

ApiResponsedata 部分強制為必填,讓呼叫端不必再檢查 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-festPartialDeepRequiredDeep)。
與交叉類型(Intersection)衝突 若同時使用 Required<T> 與其他交叉型別,可能產生意外的屬性合併結果。 盡量先 簡化型別,或使用 & 之後再套用 Required<>
與映射型別的可選屬性互相抵消 在自訂映射型別中,-? 可能被再次加上 ?,導致屬性仍為可選。 注意 屬性修飾的順序,確保 -? 最後執行。

最佳實踐

  1. 只在需要「完整」資料的地方使用:例如 API 回傳、表單送出前的驗證。避免在全部程式碼中濫用,會失去可選屬性帶來的彈性。
  2. 結合 as constRequired<>:在常量物件上使用 as const,再套用 Required<>,可以得到 不可變且必填 的型別。
  3. 使用自訂深層必填:對於多層嵌套的資料,建立 DeepRequired<T>(或使用 type-fest)以確保所有層級都被檢查。
  4. 保持型別的可讀性:若 Required<T> 讓型別變得過於龐大,可考慮 拆分子型別,只對關鍵部分套用必填。

實際應用場景

  1. 表單驗證

    • 使用者在 UI 上只填寫部分欄位,提交前透過函式把資料轉為 Required<T>,確保所有欄位都有值再送出。
  2. API 客戶端

    • 從後端取得的資料可能缺少某些欄位(因為後端回傳的是 Partial<T>),在資料處理層先轉為 Required<T>,讓後續的業務邏輯可以安全存取。
  3. 測試資料建構

    • 在單元測試中,我們常需要建立完整的測試物件。使用 Required<T> 可以快速從最小的樣本資料產生完整物件,減少手動填寫的工作量。
  4. 函式返回值保證

    • 某些函式會根據條件補全缺失欄位,回傳值可宣告為 Required<T>,讓呼叫端不必再做額外的 null/undefined 檢查。
  5. 第三方套件的型別改寫

    • 某些套件提供的型別是部分可選的,若你在自己的程式碼中需要全部必填,直接使用 Required<ThirdPartyType> 即可,避免自行複製介面。

總結

Required<T> 是 TypeScript 提供的 簡潔且強大的工具型別,讓開發者可以在不改動原始介面的情況下,快速把所有可選屬性變為必填。透過本文的說明與範例,你應該已經了解:

  • Required<T> 的基本語法與底層原理
  • 如何在 泛型函式API 回傳表單驗證 等情境中靈活運用
  • 常見的陷阱(如 undefined 型別仍在)與解決方案(自訂 DeepRequiredNonNullable 等)
  • 最佳實踐:只在需要完整資料的地方使用、保持型別可讀性、結合深層必填技巧

掌握 Required<T> 後,你的 TypeScript 程式碼將更具型別安全可維護性,也能在開發過程中更早捕捉到遺漏的欄位,提升整體開發效率。祝你在 TypeScript 的旅程中玩得開心、寫得順手!